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).
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.
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 );
}
}
}
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());
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With