Полиморфная модель связывания
Этот вопрос задавался ранее в более ранних версиях MVC. Есть также эта запись в блоге о способе обойти проблему. Мне интересно, если MVC3 представил что-нибудь, что может помочь, или есть какие-либо другие варианты.
В двух словах. Вот ситуация. У меня есть абстрактная базовая модель и 2 конкретных подкласса. У меня строго типизированное представление, которое отображает модели с EditorForModel()
, Затем у меня есть пользовательские шаблоны для рендеринга каждого конкретного типа.
Проблема приходит в пост время. Если я заставлю метод post action принять базовый класс в качестве параметра, то MVC не сможет создать его абстрактную версию (которую я бы в любом случае не хотел, я бы хотел, чтобы он создавал конкретный конкретный тип). Если я создаю несколько методов после действия, которые различаются только сигнатурой параметра, то MVC жалуется, что это неоднозначно.
Поэтому, насколько я могу судить, у меня есть несколько вариантов решения этой проблемы. Мне не нравится ни один из них по разным причинам, но я перечислю их здесь:
- Создайте пользовательское связующее для моделей, как предлагает Дарин в первом посте, на который я ссылаюсь.
- Создайте атрибут дискриминатора, как подсказывает второй пост, на который я ссылаюсь.
- Публиковать различные методы действий в зависимости от типа
- ???
Мне не нравится 1, потому что это в основном скрытая конфигурация. Некоторые другие разработчики, работающие над кодом, могут не знать об этом и тратить много времени, пытаясь выяснить, почему что-то ломается, когда что-то меняет.
Мне не нравятся 2, потому что это кажется немного хакерским. Но я склоняюсь к этому подходу.
Мне не нравится 3, потому что это означает нарушение СУХОГО.
Любые другие предложения?
Редактировать:
Я решил пойти по методу Дарина, но сделал небольшое изменение. Я добавил это к моей абстрактной модели:
[HiddenInput(DisplayValue = false)]
public string ConcreteModelType { get { return this.GetType().ToString(); }}
Тогда скрытый автоматически генерируется в моем DisplayForModel()
, Единственное, что вы должны помнить, что если вы не используете DisplayForModel()
, вам придется добавить это самостоятельно.
4 ответа
Поскольку я, очевидно, выбираю вариант 1 (:-)), позвольте мне попытаться уточнить его немного больше, чтобы он был менее хрупким и избежать жесткого кодирования конкретных экземпляров в подшивку модели. Идея состоит в том, чтобы передать конкретный тип в скрытое поле и использовать отражение для создания экземпляра конкретного типа.
Предположим, что у вас есть следующие модели представления:
public abstract class BaseViewModel
{
public int Id { get; set; }
}
public class FooViewModel : BaseViewModel
{
public string Foo { get; set; }
}
следующий контроллер:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new FooViewModel { Id = 1, Foo = "foo" };
return View(model);
}
[HttpPost]
public ActionResult Index(BaseViewModel model)
{
return View(model);
}
}
соответствующий Index
Посмотреть:
@model BaseViewModel
@using (Html.BeginForm())
{
@Html.Hidden("ModelType", Model.GetType())
@Html.EditorForModel()
<input type="submit" value="OK" />
}
и ~/Views/Home/EditorTemplates/FooViewModel.cshtml
шаблон редактора:
@model FooViewModel
@Html.EditorFor(x => x.Id)
@Html.EditorFor(x => x.Foo)
Теперь у нас может быть следующее пользовательское связующее для модели:
public class BaseViewModelBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
var typeValue = bindingContext.ValueProvider.GetValue("ModelType");
var type = Type.GetType(
(string)typeValue.ConvertTo(typeof(string)),
true
);
if (!typeof(BaseViewModel).IsAssignableFrom(type))
{
throw new InvalidOperationException("Bad Type");
}
var model = Activator.CreateInstance(type);
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, type);
return model;
}
}
Фактический тип выводится из значения ModelType
скрытое поле. Это не жестко закодировано, это означает, что вы можете добавить другие дочерние типы позже, даже не касаясь этой подшивки модели.
Этот же метод может быть легко применен к коллекциям моделей базового вида.
Я только что подумал о интересном решении этой проблемы. Вместо того, чтобы использовать привязку к параметру bsed модели следующим образом:
[HttpPost]
public ActionResult Index(MyModel model) {...}
Вместо этого я могу использовать TryUpdateModel(), чтобы определить, к какой модели привязывать код. Например я делаю что-то вроде этого:
[HttpPost]
public ActionResult Index() {...}
{
MyModel model;
if (ViewData.SomeData == Something) {
model = new MyDerivedModel();
} else {
model = new MyOtherDerivedModel();
}
TryUpdateModel(model);
if (Model.IsValid) {...}
return View(model);
}
В любом случае, это работает намного лучше, потому что, если я делаю какую-либо обработку, я должен был бы привести модель к тому, чем она на самом деле является, или использовать is
чтобы выяснить правильную карту для вызова с AutoMapper.
Я думаю, те из нас, кто не использовал MVC с первого дня, забывают о UpdateModel
а также TryUpdateModel
, но он все еще имеет свое применение.
Мне понадобился хороший день, чтобы найти ответ на тесно связанную проблему - хотя я не уверен, что это точно такая же проблема, я публикую ее здесь на тот случай, если другие ищут решение той же самой проблемы.
В моем случае у меня есть абстрактный базовый тип для ряда различных типов моделей представления. Итак, в основной модели представления у меня есть свойство абстрактного базового типа:
class View
{
public AbstractBaseItemView ItemView { get; set; }
}
У меня есть несколько подтипов AbstractBaseItemView, многие из которых определяют свои собственные эксклюзивные свойства.
Моя проблема в том, что связыватель модели не смотрит на тип объекта, присоединенного к View.ItemView, а вместо этого смотрит только на объявленный тип свойства, который представляет собой AbstractBaseItemView, и решает связать только свойства, определенные в абстрактном типе, игнорирование свойств, специфичных для конкретного типа AbstractBaseItemView, который используется.
Обходной путь для этого не очень хорош:
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
// ...
public class ModelBinder : DefaultModelBinder
{
// ...
override protected ICustomTypeDescriptor GetTypeDescriptor(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType.IsAbstract && bindingContext.Model != null)
{
var concreteType = bindingContext.Model.GetType();
if (Nullable.GetUnderlyingType(concreteType) == null)
{
return new AssociatedMetadataTypeTypeDescriptionProvider(concreteType).GetTypeDescriptor(concreteType);
}
}
return base.GetTypeDescriptor(controllerContext, bindingContext);
}
// ...
}
Хотя это изменение кажется хакерским и очень "системным", оно, похоже, работает - и, насколько я могу судить, не представляет значительного риска для безопасности, поскольку не связано с CreateModel() и, следовательно, не позволяет вам опубликовать что угодно и обмануть модель-связующего в создании просто любого объекта.
Он также работает только тогда, когда объявленный тип свойства является абстрактным типом, например, абстрактным классом или интерфейсом.
В связи с этим мне приходит в голову, что другие реализации, которые я видел здесь, которые переопределяют CreateModel(), вероятно, будут работать только при публикации совершенно новых объектов - и будут страдать от той же проблемы, с которой я столкнулся, когда объявленное свойство -тип имеет абстрактный тип. Таким образом, вы, скорее всего, не сможете редактировать определенные свойства конкретных типов на существующих модельных объектах, а только создавать новые.
Другими словами, вам, вероятно, потребуется интегрировать этот обходной путь в связыватель, чтобы также иметь возможность правильно редактировать объекты, которые были добавлены в модель представления перед привязкой... Лично я считаю, что это более безопасный подход, так как Я контролирую, какой конкретный тип добавляется - так что контроллер / действие может косвенным образом указывать конкретный тип, который может быть связан, просто заполняя свойство пустым экземпляром.
Я надеюсь, что это полезно для других...
Используя метод Дарина для различения типов вашей модели через скрытое поле в вашем представлении, я бы порекомендовал вам использовать пользовательские RouteHandler
чтобы различать типы вашей модели и направлять каждого к уникально названному действию на вашем контроллере. Например, если у вас есть две конкретные модели, Foo и Bar, для вашего Create
действие в вашем контроллере, сделать CreateFoo(Foo model)
действие и CreateBar(Bar model)
действие. Затем создайте пользовательский RouteHandler следующим образом:
public class MyRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
var httpContext = requestContext.HttpContext;
var modelType = httpContext.Request.Form["ModelType"];
var routeData = requestContext.RouteData;
if (!String.IsNullOrEmpty(modelType))
{
var action = routeData.Values["action"];
routeData.Values["action"] = action + modelType;
}
var handler = new MvcHandler(requestContext);
return handler;
}
}
Затем в Global.asax.cs измените RegisterRoutes()
следующее:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
AreaRegistration.RegisterAllAreas();
routes.Add("Default", new Route("{controller}/{action}/{id}",
new RouteValueDictionary(
new { controller = "Home",
action = "Index",
id = UrlParameter.Optional }),
new MyRouteHandler()));
}
Затем, когда поступит запрос Create, если ModelType определен в возвращаемой форме, RouteHandler добавит ModelType к имени действия, позволяя определить уникальное действие для каждой конкретной модели.