Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MVC 3 Model Binding a Sub Type (Abstract Class or Interface)

Say I have a Product model, the Product model has a property of ProductSubType (abstract) and we have two concrete implementations 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 UI, user has a dropdown, they can select the product type and the input elements are displayed according to the right product type. I have all of this figured out (using an ajax get on dropdown change, return a partial/editor template and re-setup the jquery validation accordingly).

Next I created a custom model binder 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 the values correctly and works for the most part, except I lose the server side validation. So on a hunch that I am doing this incorrectly I did some more searching and came across this answer by Darin Dimitrov:

ASP.NET MVC 2 - Binding To Abstract Model

So I switched the model binder to only override CreateModel, but now it doesn't 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;     } 

Stepping though the MVC 3 src, it seems like in BindProperties, the GetFilteredModelProperties returns an empty result, and I think is because bindingcontext model is set to ProductTypeBase which doesn't have any properties.

Can anyone spot what I am doing wrong? This doesn't seem like it should be this difficult. I am sure I am missing something simple...I have another alternative in mind of instead of having a SubProduct property in the Product model to just have separate properties for Shirt and Pants. These are just View/Form models so I think that would work, but would like to get the current approach working if anything to understand what is going on...

Thanks for any help!

Update:

I didn't make it clear, but the custom model binder I added, inherits from the DefaultModelBinder

Answer

Setting ModelMetadata and Model was the missing piece. 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);          } 
like image 379
B Z Avatar asked Feb 23 '12 17:02

B Z


People also ask

What is two way binding MVC?

In two-way data binding, any changes in the Model gets reflected in the View and similarly any changes in the View gets reflected in the View automatically. This is done using attribute bind.

Does MVC use data binding?

MVC doesn't use data bindings like old web api. You have to use model bindings in a MVC or MVVM approach.

What is bind property in MVC?

Model binding allows you map request parameters to actions. This means action methods will have one or more parameters and those parameters will receive their values from the model binding framework.


2 Answers

This can be achieved through overriding CreateModel(...). I will demonstrate that with an example.

1. Lets 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 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 Default ModelBinder in global.asax This is done to set a default model binder for all actions, for a single action we can use ModelBinder attribute in action parameters)

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

5. Now we can create 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, Controller will receive an Object of type "MyDerievedClass". Validations will happen as expected.

enter image description here

like image 131
Manas Avatar answered Sep 29 '22 10:09

Manas


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

The documentation is outdated but if you look at the samples it's pretty easy.

You'll have to register your types in the 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> } 
like image 22
Marcel de Castilho Avatar answered Sep 29 '22 10:09

Marcel de Castilho