ASP.NET MVC 3: DefaultModelBinder с наследованием/полиморфизмом
Во-первых, извините за большой пост (сначала я попытался провести некоторое исследование) и для сочетания технологий по одному и тому же вопросу (ASP.NET MVC 3, Ninject и MvcContrib).
Я разрабатываю проект с ASP.NET MVC 3 для обработки некоторых клиентских заказов.
Вкратце: У меня есть некоторые объекты, унаследованные от и абстрактного класса Order
, и мне нужно их проанализировать, когда к моему контроллеру будет отправлен запрос POST. Как я могу разрешить правильный тип? Нужно ли переопределять класс DefaultModelBinder
или есть какой-то другой способ сделать это? Может ли кто-нибудь предоставить мне код или другие ссылки о том, как это сделать? Любая помощь будет замечательной!
Если сообщение сбивает с толку, я могу сделать любое изменение, чтобы было ясно!
Итак, у меня есть следующее дерево наследования для заказов, которые мне нужно обрабатывать:
public abstract partial class Order {
public Int32 OrderTypeId {get; set; }
/* rest of the implementation ommited */
}
public class OrderBottling : Order { /* implementation ommited */ }
public class OrderFinishing : Order { /* implementation ommited */ }
Все классы генерируются Entity Framework, поэтому я не буду изменять их, потому что мне нужно будет обновить модель (я знаю, что могу их расширить). Кроме того, будет больше заказов, но все они получены из Order
.
У меня есть общий вид (Create.aspx
), чтобы создать заказ, и это представление вызывает строго типизированное частичное представление для каждого из унаследованных ордеров (в данном случае OrderBottling
и OrderFinishing
). Я определил метод Create()
для запроса GET и другого для запроса POST в классе OrderController
. Второй вариант выглядит следующим образом:
public class OrderController : Controller
{
/* rest of the implementation ommited */
[HttpPost]
public ActionResult Create(Order order) { /* implementation ommited */ }
}
Теперь проблема: когда я получаю запрос POST с данными из формы, связующее по умолчанию MVC пытается создать экземпляр объекта Order
, который является ОК, так как тип метода таков. Но поскольку Order
является абстрактным, он не может быть создан, что и должно делать.
Вопрос:, как я могу узнать, какой конкретный тип Order
отправляется представлением?
Я уже искал здесь в Qaru и много рассказывал об этом (я работаю над этой проблемой уже около 3 дней!) и нашел некоторые способы решения некоторых подобных проблем, но я ничего не мог найти как моя настоящая проблема. Два варианта решения этого вопроса:
- переопределить ASP.NET MVC
DefaultModelBinder
и использовать Direct Injection, чтобы узнать, какой тип является Order
;
- создайте метод для каждого порядка (не красивый и будет проблематичным для поддержания).
Я не пробовал второй вариант, потому что я не думаю, что это правильный способ решить проблему. Для первого варианта я попробовал Ninject, чтобы разрешить тип заказа и создать его экземпляр. Мой модуль Ninject выглядит следующим образом:
private class OrdersService : NinjectModule
{
public override void Load()
{
Bind<Order>().To<OrderBottling>();
Bind<Order>().To<OrderFinishing>();
}
}
Я попытался получить один из типов с помощью метода Ninject Get<>()
, но он говорит мне, что это более чем один из способов разрешения типа. Итак, я понимаю, что модуль не очень хорошо реализован. Я также попытался реализовать подобное для обоих типов: Bind<Order>().To<OrderBottling>().WithPropertyInject("OrderTypeId", 2);
, но у него такая же проблема... Каким будет правильный способ реализовать этот модуль?
Я также попытался использовать MvcContrib Model Binder. Я сделал это:
[DerivedTypeBinderAware(typeof(OrderBottling))]
[DerivedTypeBinderAware(typeof(OrderFinishing))]
public abstract partial class Order { }
и на Global.asax.cs
Я сделал это:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
ModelBinders.Binders.Add(typeof(Order), new DerivedTypeModelBinder());
}
Но это порождает исключение: System.MissingMethodException: Невозможно создать абстрактный класс. Поэтому я предполагаю, что связующее вещество не может или не может решить правильный тип.
Большое спасибо заранее!
Изменить: прежде всего, спасибо Мартину и Джейсону за ваши ответы и извините за задержку! Я пробовал оба подхода, и оба работали! Я отметил, что Мартин ответил правильно, потому что он более гибкий и отвечает некоторым потребностям моего проекта. В частности, идентификаторы для каждого запроса хранятся в базе данных, а их размещение в классе может сломать программное обеспечение, если я изменю идентификатор только в одном месте (база данных или класс). Мартин очень гибкий в этой точке.
@Martin: в моем коде я изменил строку
var concreteType = Assembly.GetExecutingAssembly().GetType(concreteTypeValue.AttemptedValue);
к
var concreteType = Assembly.GetAssembly(typeof(Order)).GetType(concreteTypeValue.AttemptedValue);
потому что мои классы, где на другом проекте (и так, на другой сборке). Я разделяю это, потому что это кажется более гибким, чем получение только исполняющей сборки, которая не может разрешать типы на внешних сборках. В моем случае все классы заказов находятся на одной и той же сборке. Это не лучше и не волшебная формула, но мне интересно поделиться этим;)
Ответы
Ответ 1
Я пытался сделать что-то подобное раньше, и я пришел к выводу, что ничего не построено, в котором это будет обрабатываться.
Вариант, с которым я пошел, состоял в том, чтобы создать мое собственное связующее устройство (хотя унаследовано от значения по умолчанию, поэтому его не слишком много кода). Он искал значение обратной связи с именем типа xxxConcreteType, где xxx был другим типом, к которому он привязывался. Это означает, что поле должно быть отправлено обратно со значением типа, который вы пытаетесь связать; в этом случае OrderConcreteType со значением либо OrderBottling, либо OrderFinishing.
Другой вариант - использовать UpdateModel или TryUpdateModel и опустить параметр из вашего метода. Вам нужно будет определить, какую модель, которую вы обновляете, перед ее вызовом (либо параметром, либо иным образом) и создать экземпляр класса заранее, тогда вы можете использовать любой метод для его всплытия
Edit:
Вот код.
public class AbstractBindAttribute : CustomModelBinderAttribute
{
public string ConcreteTypeParameter { get; set; }
public override IModelBinder GetBinder()
{
return new AbstractModelBinder(ConcreteTypeParameter);
}
private class AbstractModelBinder : DefaultModelBinder
{
private readonly string concreteTypeParameterName;
public AbstractModelBinder(string concreteTypeParameterName)
{
this.concreteTypeParameterName = concreteTypeParameterName;
}
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
var concreteTypeValue = bindingContext.ValueProvider.GetValue(concreteTypeParameterName);
if (concreteTypeValue == null)
throw new Exception("Concrete type value not specified for abstract class binding");
var concreteType = Assembly.GetExecutingAssembly().GetType(concreteTypeValue.AttemptedValue);
if (concreteType == null)
throw new Exception("Cannot create abstract model");
if (!concreteType.IsSubclassOf(modelType))
throw new Exception("Incorrect model type specified");
var concreteInstance = Activator.CreateInstance(concreteType);
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => concreteInstance, concreteType);
return concreteInstance;
}
}
}
Измените свой метод действий таким образом:
public ActionResult Create([AbstractBind(ConcreteTypeParameter = "orderType")] Order order) { /* implementation ommited */ }
Вам нужно будет указать следующее:
@Html.Hidden("orderType, "Namespace.xxx.OrderBottling")
Ответ 2
Вы можете создать custome ModelBinder, который работает, когда ваше действие принимает определенный тип и может создать объект любого типа, который вы хотите вернуть. Метод CreateModel() принимает объект ControllerContext и ModelBindingContext, который дает вам доступ к параметрам, переданным по маршруту, url querystring и post, которые вы можете использовать для заполнения вашего объекта значениями. Стандартная реализация связующего объекта преобразует значения для свойств с тем же именем, чтобы поместить их в поля объекта.
Что я здесь делаю, просто проверьте одно из значений, чтобы определить, какой тип создать, затем вызовите метод DefaultModelBinder.CreateModel(), переключая тип, который он должен создать соответствующему типу.
public class OrderModelBinder : DefaultModelBinder
{
protected override object CreateModel(
ControllerContext controllerContext,
ModelBindingContext bindingContext,
Type modelType)
{
// get the parameter OrderTypeId
ValueProviderResult result;
result = bindingContext.ValueProvider.GetValue("OrderTypeId");
if (result == null)
return null; // OrderTypeId must be specified
// I'm assuming 1 for Bottling, 2 for Finishing
if (result.AttemptedValue.Equals("1"))
return base.CreateModel(controllerContext,
bindingContext,
typeof(OrderBottling));
else if (result.AttemptedValue.Equals("2"))
return base.CreateModel(controllerContext,
bindingContext,
typeof(OrderFinishing));
return null; // unknown OrderTypeId
}
}
Установите его для использования, когда у вас есть параметр Order для ваших действий, добавив его в Application_Start() в Global.asax.cs:
ModelBinders.Binders.Add(typeof(Order), new OrderModelBinder());
Ответ 3
Вы также можете создать общий ModelBinder, который работает для всех ваших абстрактных моделей. Мое решение требует, чтобы вы добавили скрытое поле в ваш вид под названием "ModelTypeName" со значением, заданным для имени конкретного типа, который вы хотите. Однако должно быть возможно сделать эту вещь умнее и выбрать конкретный тип, сопоставив свойства типа с полями в представлении.
В вашем приложении Global.asax.cs Application_Start():
ModelBinders.Binders.DefaultBinder = new CustomModelBinder();
CustomModelBinder:
public class CustomModelBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type 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)
throw new Exception("Invalid ModelTypeName");
var concreteInstance = Activator.CreateInstance(type);
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => concreteInstance, type);
return concreteInstance;
}
return base.CreateModel(controllerContext, bindingContext, modelType);
}
}
Ответ 4
Мое решение для этой проблемы поддерживает сложные модели, которые могут содержать другой абстрактный класс, множественное наследование, коллекции или общие классы.
public class EnhancedModelBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
Type type = modelType;
if (modelType.IsGenericType)
{
Type genericTypeDefinition = modelType.GetGenericTypeDefinition();
if (genericTypeDefinition == typeof(IDictionary<,>))
{
type = typeof(Dictionary<,>).MakeGenericType(modelType.GetGenericArguments());
}
else if (((genericTypeDefinition == typeof(IEnumerable<>)) || (genericTypeDefinition == typeof(ICollection<>))) || (genericTypeDefinition == typeof(IList<>)))
{
type = typeof(List<>).MakeGenericType(modelType.GetGenericArguments());
}
return Activator.CreateInstance(type);
}
else if(modelType.IsAbstract)
{
string concreteTypeName = bindingContext.ModelName + ".Type";
var concreteTypeResult = bindingContext.ValueProvider.GetValue(concreteTypeName);
if (concreteTypeResult == null)
throw new Exception("Concrete type for abstract class not specified");
type = Assembly.GetExecutingAssembly().GetTypes().SingleOrDefault(t => t.IsSubclassOf(modelType) && t.Name == concreteTypeResult.AttemptedValue);
if (type == null)
throw new Exception(String.Format("Concrete model type {0} not found", concreteTypeResult.AttemptedValue));
var instance = Activator.CreateInstance(type);
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => instance, type);
return instance;
}
else
{
return Activator.CreateInstance(modelType);
}
}
}
Как вы видите, вам нужно добавить поле (имя Тип), которое содержит информацию о том, какой конкретный класс наследуется от абстрактного класса. Например, классы: класс abstract Content, class TextContent, Content должен иметь тип, заданный как "TextContent".
Не забудьте переключить связующее устройство по умолчанию в global.asax:
protected void Application_Start()
{
ModelBinders.Binders.DefaultBinder = new EnhancedModelBinder();
[...]
Для получения дополнительной информации и проверки образца проекта следуйте ссылка.
Ответ 5
Измените строку:
var concreteType = Assembly.GetExecutingAssembly().GetType(concreteTypeValue.AttemptedValue);
Для этого:
Type concreteType = null;
var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var assembly in loadedAssemblies)
{
concreteType = assembly.GetType(concreteTypeValue.AttemptedValue);
if (null != concreteType)
{
break;
}
}
Это наивная реализация, которая проверяет каждую сборку для типа. Я уверен, что есть более разумные способы сделать это, но это работает достаточно хорошо.