Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET MVC 2 - Binding To Abstract Model

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?

like image 842
RPM1984 Avatar asked Oct 25 '10 06:10

RPM1984


3 Answers

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.

like image 108
Darin Dimitrov Avatar answered Nov 18 '22 19:11

Darin Dimitrov


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);
    }
}
like image 40
Kelly Avatar answered Nov 18 '22 21:11

Kelly


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.

like image 1
Andrew Barber Avatar answered Nov 18 '22 20:11

Andrew Barber