3

Not a duplicate, see appended clarification

I would like to bind a model setup like the following

public class Shop{
    public string Name {get;set;}
    public ICollection<Product> Products {get;set;} //Product is abstract
}

public abstract class Product{
    public string Name {get;set;}
}

public class ProductA : Product{
    public string foo {get;set;}
}

public class ProductB :Product{
    public string bar {get;set;}
}

And a controller like so

public ActionResult(){
    Shop model = ShopFactory.GetShop();
    return View(model);
}

[HttpPost]
public ActionResult(Shop model){
    //....
}

I'm using BeginCollectionItem to bind the collection, however a problem arrises when POSTing the form as it cannot create an abstract class - namely objects inside Shop.Products

I've looked at subclassing DefaultModelBinder to override CreateModel however CreateModel is never called with the argument modeltype = Product, only modeltype = Shop

How do I create a ModelBinder that will bind an object that has an abstract collection as a property?

Clarification
This question is not a duplicate because we are not dealing with an abstract model, we are dealing with a Model that has a collection of abstract objects, this undergoes a separate process in the model binding system.

MrJD
  • 1,879
  • 1
  • 23
  • 40
  • possible duplicate of [MVC 3 Model Binding a Sub Type (Abstract Class or Interface)](http://stackoverflow.com/questions/9417888/mvc-3-model-binding-a-sub-type-abstract-class-or-interface) – Erik Philips Jul 22 '13 at 06:41
  • Not a duplicate. This question has a collection of abstract objects. The solution in that question doesn't work. – MrJD Jul 22 '13 at 07:01

3 Answers3

3

Answered,

The issue I had was that I was creating a ModelBinder for Shop, instead of Products.

Simple.

Update
Since this got downvoted I thought I'd clarify.

I was attempting to modal bind the Shop class because that was the class I was sending to the view. In my head that made sense because that was the Model I was binding to. The issue was that a method in DefaultModelBinder called CreateModel when it came across any complex object in an IEnumerable. So the solution was to subclass DefaultModelBinder

public class ProductModelBinder : DefaultModelBinder{
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
        if (modelType.Equals(typeof(Product)))
        {
            //For now only support Product1's
            // Todo: Add support for different types
            Type instantiationType = typeof(Product1);
            var obj = Activator.CreateInstance(instantiationType);
            bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, instantiationType);
            bindingContext.ModelMetadata.Model = obj;
            return obj;
        }

        return base.CreateModel(controllerContext, bindingContext, modelType);
    }

and register said subclass to binders:

ModelBinders.Binders.Add(new KeyValuePair<Type, IModelBinder>(typeof(FormItem), new Forms.Mvc.FormItemModelBinder()));
MrJD
  • 1,879
  • 1
  • 23
  • 40
  • I had similar scenario, Concrete model with a collection of Abstract properties, this helped me get to the answer, thanks! – Sully Apr 13 '15 at 18:54
3

Just to clarify, for others like me who stumbled on this page looking for a solution:

MVC 3 Model Binding a Sub Type (Abstract Class or Interface) is exactly what you are looking for. You do not need to do anything special because of BeginCollectionItem.

Simply write a model binder for whatever abstract class you have a collection of, and decorate the abstract class with:

[ModelBinder(typeof(MyModelBinder))]
public abstract class MyClass { ...

Then in your ModelBinder, use something like this:

string typeName = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + ".type").AttemptedValue;
Type instantiationType = Type.GetType(typeName);

to get the type from your EditorTemplate's hidden field:

    @Html.HiddenFor(m => type)

Finally, if you still can't get the properties of your concrete types, double check that they are in fact properties, and not fields!

Community
  • 1
  • 1
Ollyver
  • 359
  • 2
  • 14
0

I think you should write custom model binder.please see How to Bind a collection of Abstract Class or Interface to a Model – ASP.NET MVC and MVC 3 Model Binding a Sub Type (Abstract Class or Interface).

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

}
Community
  • 1
  • 1
  • I've read all of them already. As stated in the question, the CreateModel method is not triggered with the `modeltype` argument as `Product`, it only ever triggers with the argument as `Shop` – MrJD Jul 22 '13 at 07:03
  • I'll clarify that - since the abstract class is a property of the model this method won't help as it is only called for creating the model - not properties of said model – MrJD Jul 23 '13 at 05:12