Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET MVC - Complex model validation

I have a ViewModel class like this:

class CaseModel {
    public Boolean     ClientPresent { get; set; }
    public ClientModel Client        { get; set; }
}

class ClientModel {
    [Required]
    public String      FirstName     { get; set; }
    [Required]
    public String      LastName      { get; set; }
}

The view page consists of a <input type="checkbox" name="ClientPresent" /> and a Html.EditorFor( m => m.Client ) partial view.

The idea being that when the user if providing information about a case (a business-domain object) that they can choose to not specify any information about the client (another biz object) by unchecking the ClientPresent box.

I want ASP.NET MVC to not perform any validation of the child ClientModel object - however the CaseModel.Client property is automatically populated when a form is POSTed back to the server, but because FirstName and LastName aren't (necessarily) provided by the user it means it fails the [Required] validation attributes, consequently ViewData.ModelState.IsValid returns false and the user gets a validation error message.

How can I get it so CaseModel.Client will not be validated if CaseModel.ClientPresent is false?

Note that ClientModel is a fully independent ViewModel class and is used elsewhere in the application (such as in the ClientController class which lets the user edit individual instances of Clients).

like image 727
Dai Avatar asked Jun 14 '12 02:06

Dai


2 Answers

I recognise that my problem is not to do with binding but actually with validation: by keeping the values it means the same form fields will be populated when the user reloads the page, I just needed the validation messages to be discarded as they weren't applicable.

To that end I realised I can perform the model property validation, but then use some custom logic to remove the validation messages. Here's something similar to what I did:

public class CaseModel {
    public void CleanValidation(ModelStateDictionary dict) {
        if( this.ClientPresent ) {
            dict.Keys.All( k => if( k.StartsWith("Client") dict[k].Errors.Clear() );
        }
    }
}

(Obviously my actual code is more robust, but you get the general idea)

The CleanValidation method is called directly by the controller's action method:

public void Edit(Int64 id, CaseModel model) {
    model.CleanValidation( this.ModelState );
}

I can probably tidy this up by adding CleanValidation as a method to a new interface IComplexModel and having a new model binder automatically call this method so the controller doesn't need to call it itself.

Update:

I have this interface which is applied to any ViewModel that requires complicated validation:

public interface ICustomValidation {

    void Validate(ModelStateDictionary dict);
}

In my original example, CaseModel now looks like this:

 public class CaseClientModel : ICustomValidation {

      public Boolean ClientIsNew { get; set; } // bound to a radio-button
      public ClientModel ExistingClient { get; set; } // a complex viewmodel used by a partial view
      public ClientModel NewClient { get; set; } // ditto

      public void Validate(ModelStateDictionary dict) {

          // RemoveElementsWithPrefix is an extension method that removes all key/value pairs from a dictionary if the key has the specified prefix.
          if( this.ClientIsNew ) dict.RemoveElementsWithPrefix("ExistingClient");
          else                   dict.RemoveElementsWithPrefix("NewClient");
      }
 }

The validation logic is invoked by OnActionExecuting in my common BaseController class:

protected override void OnActionExecuting(ActionExecutingContext filterContext) {
    base.OnActionExecuting(filterContext);
    if( filterContext.ActionParameters.ContainsKey("model") ) {

        Object                    model = filterContext.ActionParameters["model"];
        ModelStateDictionary modelState = filterContext.Controller.ViewData.ModelState; // ViewData.Model always returns null at this point, so do this to get the ModelState.

        ICustomValidation modelValidation = model as ICustomValidation;
        if( modelValidation != null ) {
            modelValidation.Validate( modelState );
        }
    }
}
like image 142
Dai Avatar answered Nov 11 '22 13:11

Dai


You have to create a custom model binder by inheriting from the default model binder.

  public class CustomModelBinder: DefaultModelBinder
  {
    protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
    {
      if (propertyDescriptor.Name == "Client")
      {
          var clientPresent = bindingContext.ValueProvider.GetValue("ClientPresent");

          if (clientPresent == null || 
                string.IsNullOrEmpty(clientPresent.AttemptedValue))
              return;
      }

      base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
    }
  }

Global.asax.cs

ModelBinders.Binders.Add(typeof(CaseModel), new CustomModelBinder());
like image 40
VJAI Avatar answered Nov 11 '22 12:11

VJAI