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?
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.
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);
}
}
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.
来源:https://stackoverflow.com/questions/4012217/asp-net-mvc-2-binding-to-abstract-model