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); }