MVC generic ViewModel MVC generic ViewModel asp.net asp.net

MVC generic ViewModel


I have successfully got covariant to work, that is, binding the View to an abstract base class. In fact, what I have is a binding to a List<MyBaseClass>. Then I create a specific View strongly typed to each subclass. That takes care of the binding.

But rebinding will fail because the DefaultModelBinder only knows about abstract base class and you will get an exception like, 'Cannot create abstract class'. The solution is to have a property on your base class like this:

    public virtual string BindingType    {        get        {            return this.GetType().AssemblyQualifiedName;        }    }

Bind that to a hidden input in your view. Then your replace your default ModelBinder with a custom one in Global.asax:

    // Replace default model binder with one that can deal with BaseParameter, etc.    ModelBinders.Binders.DefaultBinder = new CustomModelBinder();

And in your custom model binder you intercept the bind. If it is for one of your known abstract types, you parse the BindingType property and replace the model type so you get an instance of the subclass:

    public class CustomModelBinder : DefaultModelBinder{    private static readonly ILog logger = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, System.Type modelType)    {        if (modelType.IsInterface || modelType.IsAbstract)        {            // This is our convention for specifying the actual type of a base type or interface.            string key = string.Format("{0}.{1}", bindingContext.ModelName, Constants.UIKeys.BindingTypeProperty);                        var boundValue = bindingContext.ValueProvider.GetValue(key);            if (boundValue != null && boundValue.RawValue != null)            {                string newTypeName = ((string[])boundValue.RawValue)[0].ToString();                logger.DebugFormat("Found type override {0} for Abstract/Interface type {1}.", modelType.Name, newTypeName);                try                {                    modelType = System.Type.GetType(newTypeName);                }                catch (Exception ex)                {                    logger.ErrorFormat("Error trying to create new binding type {0} to replace original type {1}. Error: {2}", newTypeName, modelType.Name, ex.ToString());                    throw;                }            }        }        return base.CreateModel(controllerContext, bindingContext, modelType);    }    protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)    {        if (propertyDescriptor.ComponentType == typeof(BaseParameter))        {            string match = ".StringValue";            if (bindingContext.ModelName.EndsWith(match))            {                logger.DebugFormat("Try override for BaseParameter StringValue - looking for real type's Value instead.");                string pattern = match.Replace(".", @"\.");                string key = Regex.Replace(bindingContext.ModelName, pattern, ".Value");                var boundValue = bindingContext.ValueProvider.GetValue(key);                if (boundValue != null && boundValue.RawValue != null)                {                // Do some work here to replace the base value with a subclass value...                    return value;                }            }        }        return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);    }}

Here, my abstract class is BaseParameter and I am replacing the StringValue property with a different value from the subclass (not shown).

Note that although you can rebind to the correct type, the form values associated only with the subclass will not be automatically roundtripped because the modelbinder only see the properties on the base class. In my case, I only had to replace one value in GetValue and get it instead from the subclass, so it was easy. If you need to bind lots of subclass properties you will need to do a bit more work and fetch them out the form (ValueProvider[0]) and populate the instance yourself.

Note that you can add a new model binder for a specific type so you could avoid the generic type checking.


Make your ViewModel class implement a non-generic (or generic covariant) interface, then modify the view to take that interface instead of the concrete class.


Your code is almost correct; you just need to pass a HomeViewModel<IPerson> (not FakePerson) in the controller.

You could also use a covariant interface for the Model in the view, but that's probably overkill.