12

If i have the following strongly-typed view:

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<XXX.DomainModel.Core.Locations.Location>" %>

Where Location is an abstract class.

And i have the following Controller, which accepts a strongly-typed Model via a POST:

[HttpPost]
public ActionResult Index(Location model)

I get a runtime error stating "Cannot Create Abstract Class

Which of course makes sense. However - i'm not sure what the best solution is here.

I have many concrete types (around 8), and this is a view where you can only edit properties of the abstract class.

What i've tried to do is create overloads for all the different concrete types, and perform my logic in a common method.

[HttpPost]
public ActionResult Index(City model)
{
   UpdateLocationModel(model);
   return View(model);
}

[HttpPost]
public ActionResult Index(State model)
{
   UpdateLocationModel(model);
   return View(model);
}

etc etc

And then:

[NonAction]
private void UpdateLocationModel (Location model)
{
   // ..snip - update model
}

But this doesn't work either, MVC complains the action methods are ambiguous (also makes sense).

What do we do? Can we simply not bind to an abstract model?

RPM1984
  • 72,246
  • 58
  • 225
  • 350

3 Answers3

7

How about writing a custom model binder for this abstract class:

public class CustomBinder : DefaultModelBinder
{
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
        // TODO: based on some request parameter choose the proper child type
        // to instantiate here
        return new Child();
    }
}

This make sense only if you have a form where input elements are inserted dynamically based on some user action. In this case you need to pass some additional parameter to indicate which concrete class you need. Otherwise I would stick to concrete view models as action parameters.

Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • Well this is a very simple view, edit fields for properties on the abstract model. So i didn't want to have to "duplicate" this HTML across multiple strongly-typed views. I'll have a look into custom model binders - as i do know the child type when i render the view. I'll try it out the office tomorrow - cheers. – RPM1984 Oct 25 '10 at 07:41
  • Okay i've had a read of model binders, and i agree - it doesn't make sense in my scenario. I'm going to stick with the concrete view models. This would work though, so i'll accept your answer. Thanks. – RPM1984 Oct 25 '10 at 22:53
4

You can also build a generic ModelBinder that works for all of your abstract models. My solution requires you to add a hidden field to your view called 'ModelTypeName' with the value set to the name of the concrete type that you want. However, it should be possible to make this thing smarter and pick a concrete type by matching type properties to fields in the view.

In your Global.asax.cs file in Application_Start():

ModelBinders.Binders.DefaultBinder = new CustomModelBinder();

CustomModelBinder:

public class CustomModelBinder2 : DefaultModelBinder 
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var modelType = bindingContext.ModelType;
        if (modelType.IsAbstract)
        {
            var modelTypeValue = controllerContext.Controller.ValueProvider.GetValue("ModelTypeName");
            if (modelTypeValue == null)
                throw new Exception("View does not contain ModelTypeName");

            var modelTypeName = modelTypeValue.AttemptedValue;

            var type = modelType.Assembly.GetTypes().SingleOrDefault(x => x.IsSubclassOf(modelType) && x.Name == modelTypeName);

            if (type != null)
            {
                var instance= bindingContext.Model ?? base.CreateModel(controllerContext, bindingContext, type);
                bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => instance, type);
            }
        }
        return base.BindModel(controllerContext, bindingContext);
    }
}
CloudyMarble
  • 36,908
  • 70
  • 97
  • 130
Kelly
  • 3,292
  • 1
  • 24
  • 24
  • This is a helpful answer, but just for anyone applying it, I suspect there could be a security risk here if the client sends back an unexpected but legitimate type name with a harmful constructor. Right? – Jason Kleban Nov 19 '11 at 15:09
  • see also http://stackoverflow.com/questions/7222533/polymorphic-model-binding/9813472#9813472 – mindplay.dk Mar 21 '12 at 22:48
  • @uosɐſ: only in the case MVC is trying to map to a subclass (see `.IsSubclassOf(...)`) of an expected abstract class used in the controller action signature, or one of the controller action signature objects' child properties. And then it has to have a "harmful" constructor. I'm thinking of using this code. Can you give an example of such a security risk? – Joel Purra Apr 16 '12 at 18:38
  • Hmm, I guess I didn't notice that restriction when I asked that. I can't think of anything now. – Jason Kleban Apr 16 '12 at 19:24
  • @Joel If you plan to use this code, please see my edits. I have been using this version in production and it is working very well. – Kelly Apr 17 '12 at 19:50
  • @Kelly: Updated the model binder too (set `bindingContext.ModelMetadata`) before using it; [released on gist for comparison](https://gist.github.com/2415633). Thanks! – Joel Purra Apr 18 '12 at 18:56
1

Just to throw it out there - I'm very much interested in what others might answer, but this is what I ended up doing in the case where I had a similar situation;

Basically, I did not use the model class as a parameter in the Action method, instead passing in FormCollection and testing a couple known discriminators to figure out which type to create/edit, then used TryUpdateModel from there.

It seemed there might be a better way, but I'd never gotten around to thinking about it more.

Andrew Barber
  • 39,603
  • 20
  • 94
  • 123