MVC3 Input Dependent Validation

后端 未结 1 591
予麋鹿
予麋鹿 2021-01-31 23:38

Note: I\'m relatively new to MVC3.
Input validation seems to be pretty nice with this framework, where you can just say [Required] and both client and server side validation

1条回答
  •  北恋
    北恋 (楼主)
    2021-02-01 00:24

    Input validation seems to be pretty nice with this framework

    Really? The scenario you describe is a perfect example of the limitations of using data annotations for validation.

    I will try to explore 3 possible techniques. Go to the end of this answer and the third technique for the one I use and recommend.

    Let me just before start exploring them show the controller and the view that will be used for the 3 scenarios as they will be the same.

    Controller:

    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View(new MyViewModel());
        }
    
        [HttpPost]
        public ActionResult Index(MyViewModel model)
        {
            return View(model);
        }
    }
    

    View:

    @model MyViewModel
    
    @using (Html.BeginForm())
    {
        
    @Html.LabelFor(x => x.SelectedOption) @Html.DropDownListFor( x => x.SelectedOption, Model.Options, "-- select an option --", new { id = "optionSelector" } ) @Html.ValidationMessageFor(x => x.SelectedOption)
    @Html.LabelFor(x => x.Input1) @Html.EditorFor(x => x.Input1) @Html.ValidationMessageFor(x => x.Input1) @Html.LabelFor(x => x.Input2) @Html.EditorFor(x => x.Input2) @Html.ValidationMessageFor(x => x.Input2)
    @Html.Label("rad1", "Value 1") @Html.RadioButtonFor(x => x.RadioButtonValue, "value1", new { id = "rad1" }) @Html.Label("rad2", "Value 2") @Html.RadioButtonFor(x => x.RadioButtonValue, "value2", new { id = "rad2" }) @Html.ValidationMessageFor(x => x.RadioButtonValue)
    }

    script:

    $(function () {
        $('#optionSelector').change(function () {
            var value = $(this).val();
            $('#inputs').toggle(value === '1');
            $('#radios').toggle(value === '2');
        });
    });
    

    Nothing fancy here. A controller that instantiates a view model that is passed to the view. In the view we have a form and a dropdownlist. Using javascript we subscribe to the change event of this dropdownlisty and toggle different regions of this form based on the selected value.


    Possibility 1

    The first possibility is to have your view model implement the IValidatableObject. Bear in mind that if you decide to implement this interface on your view model you shouldn't use any validation attributes on your view model properties or the Validate method will never be invoked:

    public class MyViewModel: IValidatableObject
    {
        public string SelectedOption { get; set; }
        public IEnumerable Options
        {
            get
            {
                return new[]
                {
                    new SelectListItem { Value = "1", Text = "item 1" },
                    new SelectListItem { Value = "2", Text = "item 2" },
                };
            }
        }
    
        public string RadioButtonValue { get; set; }
    
        public string Input1 { get; set; }
        public string Input2 { get; set; }
    
        public IEnumerable Validate(ValidationContext validationContext)
        {
            if (SelectedOption == "1")
            {
                if (string.IsNullOrEmpty(Input1))
                {
                    yield return new ValidationResult(
                        "Input1 is required", 
                        new[] { "Input1" }
                    );
                }
                if (string.IsNullOrEmpty(Input2))
                {
                    yield return new ValidationResult(
                        "Input2 is required",
                        new[] { "Input2" }
                    );
                }
            }
            else if (SelectedOption == "2")
            {
                if (string.IsNullOrEmpty(RadioButtonValue))
                {
                    yield return new ValidationResult(
                        "RadioButtonValue is required",
                        new[] { "RadioButtonValue" }
                    );
                }
            }
            else
            {
                yield return new ValidationResult(
                    "You must select at least one option", 
                    new[] { "SelectedOption" }
                );
            }
        }
    }
    

    What's nice about this approach is that you could handle any complex validation scenario. What's bad about this approach is that it's not quite readable as we are mixing validation with messages and error input field name selection.


    Possibility 2

    Another possibility is to write a custom validation attribute like [RequiredIf]:

    [AttributeUsageAttribute(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = true)]
    public class RequiredIfAttribute : RequiredAttribute
    {
        private string OtherProperty { get; set; }
        private object Condition { get; set; }
    
        public RequiredIfAttribute(string otherProperty, object condition)
        {
            OtherProperty = otherProperty;
            Condition = condition;
        }
    
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            var property = validationContext.ObjectType.GetProperty(OtherProperty);
            if (property == null)
                return new ValidationResult(String.Format("Property {0} not found.", OtherProperty));
    
            var propertyValue = property.GetValue(validationContext.ObjectInstance, null);
            var conditionIsMet = Equals(propertyValue, Condition);
            return conditionIsMet ? base.IsValid(value, validationContext) : null;
        }
    }
    

    and then:

    public class MyViewModel
    {
        [Required]
        public string SelectedOption { get; set; }
        public IEnumerable Options
        {
            get
            {
                return new[]
                {
                    new SelectListItem { Value = "1", Text = "item 1" },
                    new SelectListItem { Value = "2", Text = "item 2" },
                };
            }
        }
    
        [RequiredIf("SelectedOption", "2")]
        public string RadioButtonValue { get; set; }
    
        [RequiredIf("SelectedOption", "1")]
        public string Input1 { get; set; }
        [RequiredIf("SelectedOption", "1")]
        public string Input2 { get; set; }
    }
    

    What's nice about this approach is that our view model is clean. What's bad about this is that using custom validation attributes you might quickly hit the limits. Think for example more complex scenarios where you would need to recurse down to sub-models and collections and stuff. This will quickly become a mess.


    Possibility 3

    A third possibility is to use FluentValidation.NET. It's what I personally use and recommend.

    So:

    1. Install-Package FluentValidation.MVC3 in your NuGet console
    2. In Application_Start in your Global.asax add the following line:

      FluentValidationModelValidatorProvider.Configure();
      
    3. Write a validator for the view model:

      public class MyViewModelValidator : AbstractValidator
      {
          public MyViewModelValidator()
          {
              RuleFor(x => x.SelectedOption).NotEmpty();
              RuleFor(x => x.Input1).NotEmpty().When(x => x.SelectedOption == "1");
              RuleFor(x => x.Input2).NotEmpty().When(x => x.SelectedOption == "1");
              RuleFor(x => x.RadioButtonValue).NotEmpty().When(x => x.SelectedOption == "2");
          }
      }
      
    4. And the view model itself is a POCO:

      [Validator(typeof(MyViewModelValidator))]
      public class MyViewModel
      {
          public string SelectedOption { get; set; }
          public IEnumerable Options
          {
              get
              {
                  return new[]
                  {
                      new SelectListItem { Value = "1", Text = "item 1" },
                      new SelectListItem { Value = "2", Text = "item 2" },
                  };
              }
          }
      
          public string RadioButtonValue { get; set; }
          public string Input1 { get; set; }
          public string Input2 { get; set; }
      }
      

    What's good about this is that we have a perfect separation between the validation and the view model. It integrates nicely with ASP.NET MVC. We can unit test our validator in isolation in a very easy and fluent way.

    What's bad about this is that when Microsoft were designing ASP.NET MVC they opted for declarative validation logic (using data annotations) instead of imperative which is much better suited to validation scenarios and can handle just anything. It's bad that FluentValidation.NET is not actually the standard way to perform validation in ASP.NET MVC.

    0 讨论(0)
提交回复
热议问题