Подтвердить что ты не робот

Полиморфная привязка модели

Этот вопрос был задан до в более ранних версиях MVC. Существует также эта запись в блоге о способе решения этой проблемы. Мне интересно, представил ли MVC3 что-нибудь, что может помочь, или если есть другие варианты.

В двух словах. Вот ситуация. У меня есть абстрактная базовая модель и 2 конкретных подкласса. У меня есть строго типизированное представление, которое отображает модели с EditorForModel(). Затем у меня есть настраиваемые шаблоны для каждого конкретного типа.

Проблема возникает в пост-время. Если я сделаю метод post action принятым базовым классом в качестве параметра, то MVC не сможет создать абстрактную версию его (чего я бы не хотел, я бы хотел, чтобы он создал конкретный конкретный тип). Если я создаю несколько методов post action, которые различаются только сигнатурой параметра, тогда MVC жалуется, что он неоднозначен.

Итак, насколько я могу судить, у меня есть несколько вариантов решения этой проблемы. Мне не нравится ни одна из них по разным причинам, но я перечислил их здесь:

  • Создайте настраиваемое связующее устройство, как предлагает Дарин в первом посте, с которым я связан.
  • Создайте атрибут дискриминатора в качестве второго сообщения, которое я связал с предложением.
  • Опубликовать различные методы действий на основе типа
  • ???

Мне не нравится 1, потому что это в основном конфигурация, которая скрыта. Некоторые другие разработчики, работающие над кодом, могут не знать об этом и тратить много времени на то, чтобы выяснить, почему вещи ломаются, когда меняются.

Мне не нравится 2, потому что это похоже на хаки. Но я склоняюсь к этому подходу.

Мне не нравится 3, потому что это означает нарушение DRY.

Любые другие предложения?

Изменить:

Я решил пойти с методом Дарина, но сделал небольшое изменение. Я добавил это к моей абстрактной модели:

[HiddenInput(DisplayValue = false)]
public string ConcreteModelType { get { return this.GetType().ToString(); }}

Затем скрытый автоматически создается в моем DisplayForModel(). Единственное, что вы должны помнить, это то, что если вы не используете DisplayForModel(), вам придется добавить его самостоятельно.

4b9b3361

Ответ 1

Так как я, очевидно, выбираю вариант 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. Он не является жестким, что означает, что вы могли бы добавить другие дочерние типы позже, не прибегая к такому связующему моменту.

Этот же метод может быть легко применен к коллекциям моделей базового вида.

Ответ 2

Я только подумал о том, чтобы решить эту проблему. Вместо использования привязки модели с параметром 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, но все еще используют его.

Ответ 3

Мне потребовался хороший день для ответа на тесно связанную проблему - хотя я не уверен, что это точно такая же проблема, я размещаю ее здесь, если другие ищут решение для того же точная проблема.

В моем случае у меня есть абстрактный базовый тип для нескольких типов типов представлений. Итак, в основной модели представления у меня есть свойство абстрактного базового типа:

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(), вероятно, будут работать только при публикации совершенно новых объектов - и будут страдать от той же проблемы, с которой я столкнулся, когда объявленный тип свойства имеет абстрактный тип. Таким образом, вы, скорее всего, не сможете редактировать конкретные свойства конкретных типов на существующих объектах модели, но только создавать новые.

Иными словами, вам, вероятно, понадобится интегрировать эту рабочую среду в ваше связующее, чтобы также иметь возможность правильно редактировать объекты, которые были добавлены в модель представления до привязки... Лично я считаю, что более безопасный подход, поскольку я контролирую добавление конкретного типа - поэтому контроллер/действие может косвенно указать конкретный тип, который может быть связан, просто заполнив свойство пустым экземпляром.

Я надеюсь, что это полезно другим...

Ответ 4

Используя метод Дарина, чтобы различать типы моделей с помощью скрытого поля в вашем представлении, я бы рекомендовал вам использовать пользовательский RouteHandler, чтобы отличать ваши типы моделей и направлять их на одноименное действие на вашем контроллере. Например, если у вас есть две конкретные модели, Foo и Bar, для вашего действия Create в вашем контроллере, выполните действие CreateFoo(Foo model) и a 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 к имени действия, позволяя определить уникальное действие для каждой конкретной модели.