Полиморфная модель связывания - PullRequest
56 голосов
/ 28 августа 2011

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

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

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

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

  1. Создайте пользовательский механизм связывания, как предлагает Дарин в первом посте, на который я ссылаюсь.
  2. Создайте атрибут дискриминатора, как подсказывает второй пост, на который я ссылался.
  3. Публикация в различных методах действий, основанных на типе
  4. ???

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

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

Мне не нравится 3, потому что это означает нарушение СУХОЙ.

Есть еще предложения?

Редактировать:

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

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

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

Ответы [ 4 ]

59 голосов
/ 28 августа 2011

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

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

14 голосов
/ 11 сентября 2011

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

7 голосов
/ 22 марта 2012

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

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

class View
{
    public AbstractBaseItemView ItemView { get; set; }
}

У меня есть несколько подтипов AbstractBaseItemView, многие из которых определяют свои собственные эксклюзивные свойства.

Моя проблема в том, что связыватель модели не смотрит на тип объекта, присоединенного к View.ItemView, а вместо этого смотрит только на объявленный тип свойства, которым является AbstractBaseItemView, и решает связать only свойства, определенные в абстрактном типе, игнорируя свойства, специфичные для конкретного типа 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 голосов
/ 28 августа 2011

Используя метод Дарина для различения типов вашей модели через скрытое поле в вашем представлении, я бы порекомендовал вам использовать пользовательский 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())); 
} 

Затем, когда запрос на созданиеЕсли в возвращаемой форме определен ModelType, RouteHandler добавит ModelType к имени действия, позволяя определить уникальное действие для каждой конкретной модели.

...