38

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?

dav_i
  • 27,509
  • 17
  • 104
  • 136
  • I think this will work: http://www.codinginstinct.com/2010/03/aspnet-mvc-and-convention-based-forms.html –  Jun 26 '11 at 16:08

2 Answers2

72

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.

Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • Cheers man, looks like I wasn't far off the Mark. Your assumed code is spot on :) –  Jun 26 '11 at 17:54
  • Does it matter that the array of TableInputModel are a part of a larger InputModel - rather than being used as direct params on the post action? –  Jun 27 '11 at 10:02
  • @iwayneo, no, the array could be a property of another view model. You just might need to look and tweak the prefix at the model binder. – Darin Dimitrov Jun 27 '11 at 11:38
  • hmmmmm... i have an issue then. i had tweaked it slightly to look like: http://monobin.com/__ff95e3f3 but it never gets called - i assumed it was because it wasn;t the topmost viewmodel... now i have no idea –  Jun 27 '11 at 11:49
  • @iwayneo, how does your main view model look like? – Darin Dimitrov Jun 27 '11 at 11:58
  • 1
    OK - i did a test project: https://github.com/wayne-o/BindingTest and that works as expected... i need to peel back my real project and figure out what's stopping it from working there. thanks for your help. –  Jun 30 '11 at 10:31
  • finally fixed this - i was defaulting the NewTable in the default ctor!!! durrrr :D –  Jun 30 '11 at 11:37
  • 5
    Just for the records: `bindingContext.ModelName` might be empty so you have to remove the dot before the Propertyname in that case. – Jan Apr 15 '13 at 09:27
2

There was "Derived Type Model Binder" in mvccontrib. But, unfortunately, there is no such binder in mvccontrib version 3