Input validation seems pretty nice with this infrastructure
Really? The script you are describing is a great example of the limitations of using data annotations for validation.
I will try to learn 3 possible methods. Go to the end of this answer and the third method that I use and recommend.
Let me, before starting to study them, show the controller and the view that will be used for 3 scenarios, since 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()) { <div> @Html.LabelFor(x => x.SelectedOption) @Html.DropDownListFor( x => x.SelectedOption, Model.Options, "-- select an option --", new { id = "optionSelector" } ) @Html.ValidationMessageFor(x => x.SelectedOption) </div> <div id="inputs"@Html.Raw(Model.SelectedOption != "1" ? " style=\"display:none;\"" : "")> @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) </div> <div id="radios"@Html.Raw(Model.SelectedOption != "2" ? " style=\"display:none;\"" : "")> @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) </div> <button type="submit">OK</button> }
script:
$(function () { $('#optionSelector').change(function () { var value = $(this).val(); $('#inputs').toggle(value === '1'); $('#radios').toggle(value === '2'); }); });
Nothing special here. The controller that creates the view model, which is passed to the view. In the view, we have a form and a drop-down list. Using javascript, we subscribe to the change event of this dropdownlisty and switch different areas of this form based on the selected value.
Opportunity 1
The first possibility is for your view model to implement IValidatableObject . Keep in mind that if you decide to implement this interface in your view model, you should not use any validation attributes in your view model properties, otherwise the Validate method will never be called:
public class MyViewModel: IValidatableObject { public string SelectedOption { get; set; } public IEnumerable<SelectListItem> 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<ValidationResult> 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 is good about this approach is that you can handle any complex validation script. What's wrong with this approach is that it is not completely readable, since we are mixing validation with messages and choosing the name of the error input field.
Opportunity 2
Another option is to write your own validation attribute, for example [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<SelectListItem> 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 is nice about this approach is that our review model is clean. What's bad is that with custom validation attributes you can quickly get within limits. Think, for example, of more complex scenarios in which you will need to move on to sub-modes and collections and the like. It will quickly become a mess.
Opportunity 3
A third possibility is to use FluentValidation.NET . This is what I personally use and recommend.
So:
Install-Package FluentValidation.MVC3 in NuGet ConsoleIn Application_Start in Global.asax add the following line:
FluentValidationModelValidatorProvider.Configure();
Write a validator for the view model:
public class MyViewModelValidator : AbstractValidator<MyViewModel> { 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"); } }
And the presentation model itself is a POCO:
[Validator(typeof(MyViewModelValidator))] public class MyViewModel { public string SelectedOption { get; set; } public IEnumerable<SelectListItem> 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 is good about this is that we have a perfect separation between validation and presentation model. This goes well with ASP.NET MVC . We can unit test our validator in a very simple and free way.
What's bad is that when Microsoft developed ASP.NET MVC, they chose declarative validation logic (using data annotations) instead of imperative, which is much better for validation scripts and can only handle anything. It is bad that FluentValidation.NET is not really the standard way to perform validation in ASP.NET MVC.