ViewModel with List<BaseClass> and editor templates

user156888 picture user156888 · Jun 26, 2011 · Viewed 12.9k times · Source

I have a view that lists tables being added to a floor plan. Tables derive from TableInputModel to allow for RectangleTableInputModel, CircleTableInputModel, etc

The ViewModel has a list of TableInputModel which are all one of the derived types.

I have a partial view for each of the derived types and given a List of mixed derived types the framework knows how to render them.

However, on submitting the form the type information is lost. I have tried with a custom model binder but because the type info is lost when it's being submitted, it wont work...

Has anyone tried this before?

Answer

Darin Dimitrov picture Darin Dimitrov · Jun 26, 2011

Assuming you have the following models:

public abstract class TableInputModel 
{ 

}

public class RectangleTableInputModel : TableInputModel 
{
    public string Foo { get; set; }
}

public class CircleTableInputModel : TableInputModel 
{
    public string Bar { get; set; }
}

And the following controller:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        var model = new TableInputModel[]
        {
            new RectangleTableInputModel(),
            new CircleTableInputModel()
        };
        return View(model);
    }

    [HttpPost]
    public ActionResult Index(TableInputModel[] model)
    {
        return View(model);
    }
}

Now you could write views.

Main view Index.cshtml:

@model TableInputModel[]
@using (Html.BeginForm())
{
    @Html.EditorForModel()
    <input type="submit" value="OK" />
}

and the corresponding editor templates.

~/Views/Home/EditorTemplates/RectangleTableInputModel.cshtml:

@model RectangleTableInputModel
<h3>Rectangle</h3>
@Html.Hidden("ModelType", Model.GetType())
@Html.EditorFor(x => x.Foo)

~/Views/Home/EditorTemplates/CircleTableInputModel.cshtml:

@model CircleTableInputModel
<h3>Circle</h3>
@Html.Hidden("ModelType", Model.GetType())
@Html.EditorFor(x => x.Bar)

and final missing peace of the puzzle is the custom model binder for the TableInputModel type which will use the posted hidden field value to fetch the type and instantiate the proper implementation:

public class TableInputModelBinder : DefaultModelBinder
{
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
        var typeValue = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + ".ModelType");
        var type = Type.GetType(
            (string)typeValue.ConvertTo(typeof(string)), 
            true
        );
        var model = Activator.CreateInstance(type);
        bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, type);
        return model;
    }
}

which will be registered in Application_Start:

ModelBinders.Binders.Add(typeof(TableInputModel), new TableInputModelBinder());

and that's pretty much all. Now inside the Index Post action the model array will be properly initialzed with correct types.