MrJD
MrJD

Reputation: 1889

MVC model binding - abstract collection properties

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.

Upvotes: 3

Views: 3409

Answers (3)

Ollyver
Ollyver

Reputation: 367

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!

Upvotes: 3

MrJD
MrJD

Reputation: 1889

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

Upvotes: 3

user1968030
user1968030

Reputation:

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

}

Upvotes: 0

Related Questions