Welcome toVigges Developer Community-Open, Learning,Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
1.7k views
in Technique[技术] by (71.8m points)

asp.net mvc - MVC3 Validation - Require One From Group

Given the following viewmodel:

public class SomeViewModel
{
  public bool IsA { get; set; }
  public bool IsB { get; set; }
  public bool IsC { get; set; } 
  //... other properties
}

I wish to create a custom attribute that validates at least one of the available properties are true. I envision being able to attach an attribute to a property and assign a group name like so:

public class SomeViewModel
{
  [RequireAtLeastOneOfGroup("Group1")]
  public bool IsA { get; set; }

  [RequireAtLeastOneOfGroup("Group1")]
  public bool IsB { get; set; }

  [RequireAtLeastOneOfGroup("Group1")]
  public bool IsC { get; set; } 

  //... other properties

  [RequireAtLeastOneOfGroup("Group2")]
  public bool IsY { get; set; }

  [RequireAtLeastOneOfGroup("Group2")]
  public bool IsZ { get; set; }
}

I would like to validate on the client-side prior to form submission as values in the form change which is why I prefer to avoid a class-level attribute if possible.

This would require both the server-side and client-side validation to locate all properties having identical group name values passed in as the parameter for the custom attribute. Is this possible? Any guidance is much appreciated.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

Here's one way to proceed (there are other ways, I am just illustrating one that would match your view model as is):

[AttributeUsage(AttributeTargets.Property)]
public class RequireAtLeastOneOfGroupAttribute: ValidationAttribute, IClientValidatable
{
    public RequireAtLeastOneOfGroupAttribute(string groupName)
    {
        ErrorMessage = string.Format("You must select at least one value from group "{0}"", groupName);
        GroupName = groupName;
    }

    public string GroupName { get; private set; }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        foreach (var property in GetGroupProperties(validationContext.ObjectType))
        {
            var propertyValue = (bool)property.GetValue(validationContext.ObjectInstance, null);
            if (propertyValue)
            {
                // at least one property is true in this group => the model is valid
                return null;
            }
        }
        return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
    }

    private IEnumerable<PropertyInfo> GetGroupProperties(Type type)
    {
        return
            from property in type.GetProperties()
            where property.PropertyType == typeof(bool)
            let attributes = property.GetCustomAttributes(typeof(RequireAtLeastOneOfGroupAttribute), false).OfType<RequireAtLeastOneOfGroupAttribute>()
            where attributes.Count() > 0
            from attribute in attributes
            where attribute.GroupName == GroupName
            select property;
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        var groupProperties = GetGroupProperties(metadata.ContainerType).Select(p => p.Name);
        var rule = new ModelClientValidationRule
        {
            ErrorMessage = this.ErrorMessage
        };
        rule.ValidationType = string.Format("group", GroupName.ToLower());
        rule.ValidationParameters["propertynames"] = string.Join(",", groupProperties);
        yield return rule;
    }
}

Now, let's define a controller:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        var model = new SomeViewModel();
        return View(model);        
    }

    [HttpPost]
    public ActionResult Index(SomeViewModel model)
    {
        return View(model);
    }
}

and a view:

@model SomeViewModel

<script src="@Url.Content("~/Scripts/jquery.validate.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.js")" type="text/javascript"></script>

@using (Html.BeginForm())
{
    @Html.EditorFor(x => x.IsA)
    @Html.ValidationMessageFor(x => x.IsA)
    <br/>
    @Html.EditorFor(x => x.IsB)<br/>
    @Html.EditorFor(x => x.IsC)<br/>

    @Html.EditorFor(x => x.IsY)
    @Html.ValidationMessageFor(x => x.IsY)
    <br/>
    @Html.EditorFor(x => x.IsZ)<br/>
    <input type="submit" value="OK" />
}

The last part that's left would be to register adapters for the client side validation:

jQuery.validator.unobtrusive.adapters.add(
    'group', 
    [ 'propertynames' ],
    function (options) {
        options.rules['group'] = options.params;
        options.messages['group'] = options.message;
    }
);

jQuery.validator.addMethod('group', function (value, element, params) {
    var properties = params.propertynames.split(',');
    var isValid = false;
    for (var i = 0; i < properties.length; i++) {
        var property = properties[i];
        if ($('#' + property).is(':checked')) {
            isValid = true;
            break;
        }
    }
    return isValid;
}, '');

Based on your specific requirements the code might be adapted.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to Vigges Developer Community for programmer and developer-Open, Learning and Share
...