Custom MVC ModelBinder with Complex Models/Objects/Interfaces using built in MVC Validation
I’ve been creating some cool stuff using ASP.Net MVC 3 lately and came across a situation where I’d like to have quite a complex model/object bound to an Action on my Controller based on a set of posted values from a form. In order to do this, a custom ModelBinder is necessary to collect the data from the posted values, turn it into my custom object, and bind that object to my Action’s parameter. The easy part is to write code to turn the posted values into my custom object and return it, the tricky part is trying to get the in-built back-end MVC validation working for my model… which is currently using DataAnnotations. I really didn’t feel like writing my own logic to validate my model against DataAnnotations and also didn’t want to write the logic to take into account that DataAnnotations might not be the current developers validation provider of choice. So after much digging through the source of MVC and using Reflector, I finally found the solution. To better describe the concept, here’s an example of the issue:
Each IMyObject has many properties: IProperty. Each IProperty is of a type: IType and each IType has a model which is used to render out the editor in MVC (EditorFor)
So my controller’s Action looks something like this:
Initially my model binder looks like this which is the ‘easy’ part that converts the posted values into an IMyObject object with all of it’s values filled in:
[ModelBinderType(typeof(IMyObject))] public class EditorModelBinder : DefaultModelBinder { protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) { if (modelType.Equals(typeof(IMyObject))) { //get the id from the posted values var id = (int)controllerContext .RouteData .Values .GetRequiredObject("id"); //get the object from my data repository by id var model = GetMyObjectFromMyDataRepository(id); foreach (var p in item.Properties) { //it's the editor model that created the editor //for each property in mvc which means that //it's this data that is being posted back var editorModel = p.DataType.EditorModel; // ... Go convert all of the posted values using the bindingContext // ValueProvider and build up the MyObject object created above // ... (A bunch of code goes here to do the conversion) // Except, now that it's created, how the heck do i run it through // MVC validation? } return model; } return base.CreateModel(controllerContext, bindingContext, modelType); } }
In order for the call in your controller to check if your ModelState.IsValid, something needs to do the validation of the model and put the validation results inside the ModelState object. This of course already exists in the MVC framework and is done with the DefaultModelBinder. The DefaultModelBinder is pretty smart and can figure out how to automagically parse and transform the posted values into the specified model and also run it through the MVC validators so long as the model is simple enough to figure out. When your model consists of interfaces, it generally can’t do much about it because it doesn’t know how to create your interface. It also has problems when the model is complex and contains sub objects of sub objects (like the IMyObject). So how do we tap in to this underlying functionality?
[ModelBinderType(typeof(IMyObject))] public class EditorModelBinder : DefaultModelBinder { protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) { if (modelType.Equals(typeof(IMyObject))) { //get the id from the posted values var id = (int)controllerContext .RouteData .Values .GetRequiredObject("id"); //get the object from my data repository by id var model = GetMyObjectFromMyDataRepository(id); foreach (var p in item.Properties) { //it's the editor model that created the editor //for each property in mvc which means that //it's this data that is being posted back var editorModel = p.DataType.EditorModel; //get a binder for the EditorModel IModelBinder binder = this.Binders .GetBinder(model.GetType()); //create a new context for it ModelBindingContext customBindingContext = new ModelBindingContext(); //get the meta data for it customBindingContext.ModelMetadata = ModelMetadataProviders .Current .GetMetadataForType(() => model, model.GetType()); //ensure we use our correct field 'prefix' //(this is optional and depends on if you are using a custom prefix) customBindingContext.ModelName = p.Id.ToString(); //use our existing model state customBindingContext.ModelState = bindingContext.ModelState; //use our existing value provider customBindingContext.ValueProvider = bindingContext.ValueProvider; //do the binding! this will also validate and put the errors into the ModelState for us. model = (object)binder.BindModel(controllerContext, customBindingContext); } return model; } return base.CreateModel(controllerContext, bindingContext, modelType); } }
The concept above is pretty much how a Controller’s TryUpdateModel method works and how it does the underlying validation.