MVC 3 Model Binding a Sub Type (abstract class or interface)

Say I have a product model, the product model has the ProductSubType (abstract) property, and we have two specific implementations of Shirt and Pants.

Here is the source:

public class Product { public int Id { get; set; } [Required] public string Name { get; set; } [Required] public decimal? Price { get; set; } [Required] public int? ProductType { get; set; } public ProductTypeBase SubProduct { get; set; } } public abstract class ProductTypeBase { } public class Shirt : ProductTypeBase { [Required] public string Color { get; set; } public bool HasSleeves { get; set; } } public class Pants : ProductTypeBase { [Required] public string Color { get; set; } [Required] public string Size { get; set; } } 

In my user interface, the user has a drop-down list, they can choose the type of product, and input elements are displayed according to the correct type of product. I understood all this (using ajax, having received the drop-down list change, return the partial / editor template and reset the jquery check accordingly).

Then I created a custom mediator for ProductTypeBase.

  public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { ProductTypeBase subType = null; var productType = (int)bindingContext.ValueProvider.GetValue("ProductType").ConvertTo(typeof(int)); if (productType == 1) { var shirt = new Shirt(); shirt.Color = (string)bindingContext.ValueProvider.GetValue("SubProduct.Color").ConvertTo(typeof(string)); shirt.HasSleeves = (bool)bindingContext.ValueProvider.GetValue("SubProduct.HasSleeves").ConvertTo(typeof(bool)); subType = shirt; } else if (productType == 2) { var pants = new Pants(); pants.Size = (string)bindingContext.ValueProvider.GetValue("SubProduct.Size").ConvertTo(typeof(string)); pants.Color = (string)bindingContext.ValueProvider.GetValue("SubProduct.Color").ConvertTo(typeof(string)); subType = pants; } return subType; } } 

This binds values ​​correctly and works for the most part, except that I am losing server-side validation. Therefore, suspecting that I was doing it wrong, I did a few more searches and came across this answer Darina Dimitrova:

ASP.NET MVC 2 - abstract model binding

So, I switched the model change only to override CreateModel, but now it does not bind the values.

 protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) { ProductTypeBase subType = null; var productType = (int)bindingContext.ValueProvider.GetValue("ProductType").ConvertTo(typeof(int)); if (productType == 1) { subType = new Shirt(); } else if (productType == 2) { subType = new Pants(); } return subType; } 

Having selected MVC 3 src, it seems that in BindProperties, GetFilteredModelProperties returns an empty result, and I think this is because the bindingText model is set to ProductTypeBase, which has no properties.

Can someone determine what I am doing wrong? It doesn't look like it should be so complicated. I am sure that I am missing something simple ... I have another alternative, instead of having the SubProduct property in the Product model, to have only separate properties for Shirt and Pants. These are just View / Form models, so I think it will work, but I would like the current approach to work if you understand something that is happening ...

Thanks for any help!

Update:

I did not make it clear, but the added custom mediator inherits from DefaultModelBinder

Answer

Installing ModelMetadata and the model was the missing part. Thanks Manas!

 protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) { if (modelType.Equals(typeof(ProductTypeBase))) { Type instantiationType = null; var productType = (int)bindingContext.ValueProvider.GetValue("ProductType").ConvertTo(typeof(int)); if (productType == 1) { instantiationType = typeof(Shirt); } else if (productType == 2) { instantiationType = typeof(Pants); } var obj = Activator.CreateInstance(instantiationType); bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, instantiationType); bindingContext.ModelMetadata.Model = obj; return obj; } return base.CreateModel(controllerContext, bindingContext, modelType); } 
+43
asp.net-mvc asp.net-mvc-3 model-binding
Feb 23 '12 at 17:20
source share
3 answers

This can be achieved by overriding CreateModel (...). I will demonstrate this with an example.

1. Let's create a model and some base and child classes .

 public class MyModel { public MyBaseClass BaseClass { get; set; } } public abstract class MyBaseClass { public virtual string MyName { get { return "MyBaseClass"; } } } public class MyDerievedClass : MyBaseClass { public int MyProperty { get; set; } public override string MyName { get { return "MyDerievedClass"; } } } 

2. Now create a modelbinder and override CreateModel

 public class MyModelBinder : DefaultModelBinder { protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) { /// MyBaseClass and MyDerievedClass are hardcoded. /// We can use reflection to read the assembly and get concrete types of any base type if (modelType.Equals(typeof(MyBaseClass))) { Type instantiationType = typeof(MyDerievedClass); var obj=Activator.CreateInstance(instantiationType); bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, instantiationType); bindingContext.ModelMetadata.Model = obj; return obj; } return base.CreateModel(controllerContext, bindingContext, modelType); } } 

3. Now in the controller, create the get and post action.

 [HttpGet] public ActionResult Index() { ViewBag.Message = "Welcome to ASP.NET MVC!"; MyModel model = new MyModel(); model.BaseClass = new MyDerievedClass(); return View(model); } [HttpPost] public ActionResult Index(MyModel model) { return View(model); } 

4. Now set MyModelBinder as the default ModelBinder in global.asax . This is done to set a standard binding object for all actions, for one action we can use the ModelBinder attribute in the action parameters)

 protected void Application_Start() { AreaRegistration.RegisterAllAreas(); ModelBinders.Binders.DefaultBinder = new MyModelBinder(); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes); } 

5. Now we can create a view of type MyModel and a partial view of type MyDerievedClass

Index.cshtml

 @model MvcApplication2.Models.MyModel @{ ViewBag.Title = "Index"; Layout = "~/Views/Shared/_Layout.cshtml"; } <h2>Index</h2> @using (Html.BeginForm()) { @Html.ValidationSummary(true) <fieldset> <legend>MyModel</legend> @Html.EditorFor(m=>m.BaseClass,"DerievedView") <p> <input type="submit" value="Create" /> </p> </fieldset> } 

DerievedView.cshtml

 @model MvcApplication2.Models.MyDerievedClass @Html.ValidationSummary(true) <fieldset> <legend>MyDerievedClass</legend> <div class="editor-label"> @Html.LabelFor(model => model.MyProperty) </div> <div class="editor-field"> @Html.EditorFor(model => model.MyProperty) @Html.ValidationMessageFor(model => model.MyProperty) </div> </fieldset> 

Now it will work as expected, the controller will receive an object of type "MyDerievedClass". Checks will be performed as expected.

enter image description here

+52
Feb 24 2018-12-12T00: 00Z
source share
β€” -

I had the same problem, I ended up using MvcContrib as sugested here .

The documentation is out of date, but if you look at the samples, it's pretty easy.

You need to register your types in Global.asax:

 protected void Application_Start(object sender, EventArgs e) { // (...) DerivedTypeModelBinderCache.RegisterDerivedTypes(typeof(ProductTypeBase), new[] { typeof(Shirt), typeof(Pants) }); } 

Add two lines to your partial views:

 @model MvcApplication.Models.Shirt @using MvcContrib.UI.DerivedTypeModelBinder @Html.TypeStamp() <div> @Html.LabelFor(m => m.Color) </div> <div> @Html.EditorFor(m => m.Color) @Html.ValidationMessageFor(m => m.Color) </div> 

Finally, in the main view (using EditorTemplates ):

 @model MvcApplication.Models.Product @{ ViewBag.Title = "Products"; } <h2> @ViewBag.Title</h2> @using (Html.BeginForm()) { <div> @Html.LabelFor(m => m.Name) </div> <div> @Html.EditorFor(m => m.Name) @Html.ValidationMessageFor(m => m.Name) </div> <div> @Html.EditorFor(m => m.SubProduct) </div> <p> <input type="submit" value="create" /> </p> } 
+4
Mar 06 '12 at 17:38
source share

ok I had the same problem and I solved in a more general way, I think. In my case, I send the object via Json from the backend to the client and from client to server:

First of all, in an abstract class, I have a field that I set in the constructor:

 ClassDescriptor = this.GetType().AssemblyQualifiedName; 

So in Json I have a ClassDescriptor field

The next step was to write a custom binder:

 public class SmartClassBinder : DefaultModelBinder { protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) { string field = String.Join(".", new String[]{bindingContext.ModelName , "ClassDescriptor"} ); var values = (ValueProviderCollection) bindingContext.ValueProvider; var classDescription = (string) values.GetValue(field).ConvertTo(typeof (string)); modelType = Type.GetType(classDescription); return base.CreateModel(controllerContext, bindingContext, modelType); } } 

And now I only need to decorate the class with an attribute. For example:

[ModelBinder (TypeOf (SmartClassBinder))] public class ConfigurationItemDescription

What is it.

+1
Jan 12 '15 at 10:50
source share



All Articles