ASP.NET MVC: Using a custom model binder to post a list of interface objects

Some background

I’m taking a large Word form and turning it into an MVC application. The form has multiple sections, and each section has a number of fields associated with it. Every section has a title and description, but the fields are all of different types – there is a comments field, a summary field, and a review field, for example. This is what my models look like.

Each section has a list of IField objects.

public class Section {
	public string Title { get; set; }
	public string Description { get; set; }
	public List<IField> Fields { get; set; }
}

(In reality, there is also a SectionViewModel object.)

The IField interface has a number of methods that must be implemented (it might be worth noting that this particular application does not use EntityFramework).

public interface IField {
	void Save();
	void Populate();
}

Finally, I’ve got a number of fields, each of which implement IField but do slightly different things. Here is what the SummaryField looks like:

public class SummaryField : IField {

	public string Summary { get; set; }

	void Save()
	{
		// Saving actions
	}

	void Populate()
	{
		// Populating actions
	}
}

Outputting form fields for each IField

Each of these sections represent a ‘page’ of this gargantuan (100+ pages of Word, people) form. The view outputs the section’s title, summary – and each IField from the List object. To do that, I just created an Editor Template for each type of IField and called EditorFor on the list object (read more about that here):

@model Section
@Html.EditorFor(x => x.SectionFields)

Depending on the type of object (SummaryField will output /Views/EditorTemplates/SummaryField.cshtml), the framework will output a different EditorTemplate.

So far so good, but what happens when you try to POST this form?

Posting a list of interfaces

If you try to post this form, MVC will throw this error: Cannot create an instance of an interface. This is because the default model binder works by creating an instance of your model (and any properties it has) and mapping the posted field names to it; Section.Title maps to the Title property on the Section object, for example. However, Section.SectionFields[0] presents a problem – you cannot create an instance of an interface (or an abstract class).

The solution is to create and register a custom model binder for the IField class. A word of warning – you could probably make this a lot more generic and reusable, but because I struggled with the examples of model binders that already exist, I’ve kept this example very simple.

Step 1 – create and register your model binder

First of all, create your model binder and inherit from the DefaultModelBinder class:

public class IFieldModelBinder : DefaultModelBinder {
	 protected override object CreateModel(
	ControllerContext controllerContext,
	ModelBindingContext bindingContext,
	Type modelType) {
	// Our work here
        }

}

Secondly, register it with your application – this is usually done in Application_Start in Global.asax. What you are doing here is telling the application that whenever you come across an instance of IField, it should look to the custom model binder instead of the default one. Note: If you find that you are creating and registering a lot of custom model binders, consider using a model binder provider to save you the trouble of registering each one individually: http://lostechies.com/jimmybogard/2011/07/07/intelligent-model-binding-with-model-binder-providers/

protected void Application_Start(Object sender, EventArgs e) {
ModelBinders.Binders.Add(typeof(IField), new IFieldModelBinder());
}

Step 2 – Casting each IField into its concrete type

Notice that we are overriding the CreateModel method in our custom model provider. This method gives us access to the incoming object type which will be IField, and the bindingContext, which has access to all posted values (form fields, query strings, etc) via the .ValueProvider.GetValue() method.

So our IField comes in. How can we tell the custom model provider what this field’s concrete type is? Since we have access to all posted values, I created two new properties and associated hidden fields in my Editor Template – one for for the field’s class name (including namespace) and one for its assembly name.:

@model SectionSummary
@Html.HiddenFor(x => x.FieldClassName)
@Html.HiddenFor(x => x.FieldAssemblyName)

Now, whenever this particular field is posted, the custom model binder will have the information about the actual type available – which means we can use it to cast the IField object and return a model that can be instantiated.

public class IFieldModelBinder : DefaultModelBinder
{
    protected override object CreateModel(
    ControllerContext controllerContext,
    ModelBindingContext bindingContext,
    Type modelType)
    {

        // Get the submitted type - should be IField
        var type = bindingContext.ModelType;

        // Get the posted 'class name' key - bindingContext.ModelName will return something like Section.FieldSections[0] in our particular context, and 'FieldClassName' is the property we're looking for
        var fieldClassName = bindingContext.ModelName + ".FieldClassName";

        // Do the same for the assembly name
        var fieldAssemblyName = bindingContext.ModelName + ".FieldAssemblyName";

        // Check that the values aren't empty/null, and use the bindingContext.ValueProvider.GetValue method to get the actual posted values

        if (!String.IsNullOrEmpty(fieldClassName) && !String.IsNullOrEmpty(fieldAssemblyName))
        {
            // The value provider returns a string[], so get the first ([0]) item
            var className = ((string[])bindingContext.ValueProvider.GetValue(fieldClassName).RawValue)[0];
            // Do the same for the assembly name
            var assemblyName =
            ((string[])bindingContext.ValueProvider.GetValue(fieldAssemblyName).RawValue)[0];

            // Once you have the assembly and the class name, get the type - I am overwriting the IField object that came in, but I do not think you have to do that
            modelType = Type.GetType(className + ", " + assemblyName);

            // Finally, create an instance of this type
            var instance = Activator.CreateInstance(modelType);

            // Update the binding context's meta data
            bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => instance, modelType);

            // Return the instance - which will now be a SummaryField or CommentField - rather than an IField
            return instance;
        }
        return null;
    }
}

You will now be able to post your List object – for each IField, the custom model binder will be called to figure out what the concrete type of each object is.

Improvements and suggestions very welcome – it took me a while to get my head around model binding, and I would love to know if there are other ways.

2 comments

  1. Hi Martina,

    I’ve been scratching my head for a while trying to get around almost the exact same problem.

    I was trying to override the BindModel method of the DefaultModelBinder though. I’m trying to test out your approach, but am having trouble following your posted CreateModel() method. Could you elaborate on the posted code. For example are you overriding the CreateModel method of the base class? What about return objects? Not all paths of your example return a value.

    I’m pretty excited by your example, but could use a little help getting over this hurdle (if you don’t mind).

    Thanks,
    Josh

    1. Hi Josh –

      I have been very sloppy – you are absolutely right; I was missing a ‘return null’ and a ‘protected override object’, and some of my variable names were inconsistent. I’ve checked the sample against the functioning original now and everything should be in order.

      I’ve updated the sample, but just in case, here is a gist from the code I use in my solution:

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s