Polymorphic model binding

This question was asked before in earlier versions of MVC. There is also this blog post about how to solve this problem. I am wondering if MVC3 presented anything that might help, or if there are other options.

In a nutshell. Here is the situation. I have an abstract base model and 2 specific subclasses. I have a strongly typed view that displays models with EditorForModel() . Then I have custom templates for each specific type.

The problem occurs in post-time. If I make the post action method an accepted base class as a parameter, then MVC cannot create an abstract version of it (which I would not want, I would like it to create a specific concrete type). If I create several post action methods that differ only in the signature of the parameter, then MVC complains that it is ambiguous.

So, as far as I can tell, I have several options for solving this problem. I don’t like any of them for various reasons, but I have listed them here:

  • Create a custom connectivity device, as Darin suggests in the first post I'm connected to.
  • Create the discriminator attribute as the second message I linked to the sentence.
  • Post various type-based action methods
  • ???

I do not like 1 because it is basically a configuration that is hidden. Some other code developers may not be aware of this and spend a lot of time figuring out why things break when they change.

I don't like 2 because it looks like hacks. But I am inclined to this approach.

I do not like 3 because it means DRY violation.

Any other suggestions?

Edit:

I decided to go with Darin's method, but made a small change. I added this to my abstract model:

 [HiddenInput(DisplayValue = false)] public string ConcreteModelType { get { return this.GetType().ToString(); }} 

Then a hidden one is automatically created in my DisplayForModel() . The only thing you should remember is that if you are not using DisplayForModel() , you will have to add it yourself.

+55
polymorphism asp.net-mvc asp.net-mvc-3 model-binding
Aug 28 '11 at 17:18
source share
4 answers

Since I obviously choose option 1 (:-)), let me try to refine it a bit more so that it is less destructible and avoid hard coding of specific instances in the binder. The idea is to pass a specific type to a hidden field and use reflection to create a specific type.

Suppose you have the following presentation models:

 public abstract class BaseViewModel { public int Id { get; set; } } public class FooViewModel : BaseViewModel { public string Foo { get; set; } } 

next controller:

 public class HomeController : Controller { public ActionResult Index() { var model = new FooViewModel { Id = 1, Foo = "foo" }; return View(model); } [HttpPost] public ActionResult Index(BaseViewModel model) { return View(model); } } 

corresponding Index view:

 @model BaseViewModel @using (Html.BeginForm()) { @Html.Hidden("ModelType", Model.GetType()) @Html.EditorForModel() <input type="submit" value="OK" /> } 

and the editor template ~/Views/Home/EditorTemplates/FooViewModel.cshtml :

 @model FooViewModel @Html.EditorFor(x => x.Id) @Html.EditorFor(x => x.Foo) 

Now we can have the following custom mediator:

 public class BaseViewModelBinder : DefaultModelBinder { protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) { var typeValue = bindingContext.ValueProvider.GetValue("ModelType"); var type = Type.GetType( (string)typeValue.ConvertTo(typeof(string)), true ); if (!typeof(BaseViewModel).IsAssignableFrom(type)) { throw new InvalidOperationException("Bad Type"); } var model = Activator.CreateInstance(type); bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, type); return model; } } 

The actual type is inferred from the value of the hidden ModelType field. It is not rigid, which means that you could add other child types later without resorting to such a connecting point.

The same method can be easily applied to collections of models of the basic form.

+58
Aug 28 '11 at 17:36
source share

I just thought about solving this problem. Instead of using model binding with the bsed parameter as follows:

 [HttpPost] public ActionResult Index(MyModel model) {...} 

Instead, I can use TryUpdateModel () so that I can determine which model to bind the code to. For example, I am doing something like this:

 [HttpPost] public ActionResult Index() {...} { MyModel model; if (ViewData.SomeData == Something) { model = new MyDerivedModel(); } else { model = new MyOtherDerivedModel(); } TryUpdateModel(model); if (Model.IsValid) {...} return View(model); } 

In any case, this works much better, because if I do any processing, then I will have to drop the model to the point that it really is anyway, or use is to determine the correct map to call using AutoMapper.

I think those of us who have not used MVC from day one forget about UpdateModel and TryUpdateModel , but still use it.

+14
Sep 11 '11 at 5:59
source share

It took me a good day to answer a closely related problem - although I'm not sure if this is exactly the same problem, I post it here if others are looking for a solution to the exact same problem.

In my case, I have an abstract base type for several types of presentation types. So, in the main view model, I have a property of an abstract base type:

 class View { public AbstractBaseItemView ItemView { get; set; } } 

I have several subtypes of AbstractBaseItemView, many of which define their own exclusive properties.

My problem is that the binder model does not look at the type of the object attached to the View.ItemView, but instead only looks at the declared property type, which is AbstractBaseItemView, and decides to bind only the properties defined in the abstract type, ignoring the properties, specific to the specific type of AbstractBaseItemView that is being used.

The work for this is pretty small:

 using System.ComponentModel; using System.ComponentModel.DataAnnotations; // ... public class ModelBinder : DefaultModelBinder { // ... override protected ICustomTypeDescriptor GetTypeDescriptor(ControllerContext controllerContext, ModelBindingContext bindingContext) { if (bindingContext.ModelType.IsAbstract && bindingContext.Model != null) { var concreteType = bindingContext.Model.GetType(); if (Nullable.GetUnderlyingType(concreteType) == null) { return new AssociatedMetadataTypeTypeDescriptionProvider(concreteType).GetTypeDescriptor(concreteType); } } return base.GetTypeDescriptor(controllerContext, bindingContext); } // ... } 

Although this change seems hacked and very β€œsystemic,” it seems to work - and as far as I can imagine, it does not pose a significant security risk, since it does not bind to CreateModel () and thus do not allow you publish something and deceive the binding to the model in creating only any object.

It also works only when the declared property type is an abstract type, for example. abstract class or interface.

In the relevant note, it occurs to me that other implementations that I saw here that override CreateModel () will probably only work when publishing completely new objects - and will suffer from the same problem that I encountered when the declared type properties has an abstract type. Thus, you most likely will not be able to edit specific properties of specific types on existing model objects, but only create new ones.

In other words, you probably need to integrate this work environment into your binder in order to also be able to correctly edit the objects that were added to the view model before binding ... Personally, I believe that it is a safer approach, since I control the addition of a specific type - therefore, the controller / action can indirectly indicate a specific type that can be connected by simply filling the property with an empty instance.

I hope this is helpful to others ...

+7
Mar 21 2018-12-21T00:
source share

Using the Darin method to distinguish between model types using a hidden field in your view, I would recommend that you use a custom RouteHandler to distinguish your model types and direct them to the action of the same name on your controller. For example, if you have two specific models, Foo and Bar, for your Create action in your controller, execute the CreateFoo(Foo model) and a CreateBar(Bar model) . Then create your own RouteHandler as follows:

 public class MyRouteHandler : IRouteHandler { public IHttpHandler GetHttpHandler(RequestContext requestContext) { var httpContext = requestContext.HttpContext; var modelType = httpContext.Request.Form["ModelType"]; var routeData = requestContext.RouteData; if (!String.IsNullOrEmpty(modelType)) { var action = routeData.Values["action"]; routeData.Values["action"] = action + modelType; } var handler = new MvcHandler(requestContext); return handler; } } 

Then in Global.asax.cs change RegisterRoutes() as follows:

 public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); AreaRegistration.RegisterAllAreas(); routes.Add("Default", new Route("{controller}/{action}/{id}", new RouteValueDictionary( new { controller = "Home", action = "Index", id = UrlParameter.Optional }), new MyRouteHandler())); } 

Then, when the Create request arrives, if the ModelType is defined in the return form, the RouteHandler will add the ModelType to the action name, allowing you to define a unique action for each specific model.

+4
Aug 28 '11 at 18:26
source share



All Articles