2011-08-28 1 views
56

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

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

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

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

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

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

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

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

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

Edit:

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

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

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

ответ

59

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

Эта же техника может быть easily be applied для коллекций моделей базового вида.

+0

Хм .. Это определенно улучшает ремонтопригодность. И это будет работать там, где ваши конкретные модели не отличаются по сигнатурам свойств (недостаток в методе Attribute). Не уверен, что мне нравится загрязнять мои взгляды этими дискриминаторами. –

+0

Я выбрал ваше решение, но сделал небольшое изменение. См. Мое редактирование. –

+0

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

4

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

+0

Эти решения зависят от наличия скрытого поля. Но это имеет две проблемы. 1), что разработчик должен помнить о добавлении скрытого поля, и 2) возможно, что кто-то может создать параметр модели с тем же именем и случайно отключить странную проблему маршрутизации. Сделав это глобальным, это приятно и удобно, но легко сломать и/или поездку. Преимущество метода атрибута заключается в том, что он влияет только на этот метод действий. –

+0

Интересный подход – BlackTigerX

14

Я только что подумал о промежуточном решении этой проблемы. Вместо использования параметра СКДЭС модели связывания, как это:

[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, но он по-прежнему пользуется им.

+1

Возможно, я сделал что-то не так, но если я просто сделаю 'TryUpdateModel (model)', он только обновит свойства и т. Д. 'MyModel'. Если я хочу, чтобы он полностью обновлял модель, мне нужно сделать' TryUpdateModel ((MyDerivedModel)), но в любом случае это отличный трюк. – Brook

7

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

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

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); 
    } 

    // ... 
} 

Хотя это изменение чувствует Hacky и очень «системный», это, кажется, работает - и не, насколько я могу понять, представляют собой значительный риск для безопасности, поскольку он не привязывается к CreateModel(), и поэтому не позволяет вам размещать все, что угодно, и обманывать привязку модели к созданию всего объекта.

Он также работает только при объявленном типе собственности аннотация, например. абстрактный класс или интерфейс.

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

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

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

+0

Это работало для меня очень хорошо. Я работал несколько дней, чтобы найти решение. Я смог создать новые объекты так же, как вы описываете, но свойства конкретного типа никогда не заселялись связующим. Теперь они есть. – davcar

+0

Я рад, что это сработало для кого-то - похоже, большинство людей не понимают эту проблему достаточно хорошо, чтобы даже заботиться ;-) –