2011-08-28 21 views
56

To pytanie zostało zignorowane asked before we wcześniejszych wersjach MVC. Istnieje również this blog entry o sposobie obejścia problemu. Zastanawiam się, czy MVC3 wprowadził coś, co może pomóc, lub jeśli są jakieś inne opcje.Wiązanie modelu polimorficznego

W pigułce. Oto sytuacja. Mam abstrakcyjny model bazowy i 2 konkretne podklasy. Mam mocno napisany widok, który renderuje modele z EditorForModel(). Następnie mam niestandardowe szablony do renderowania każdego konkretnego typu.

Problem pojawia się w momencie publikacji. Jeśli zrobię metodę post action, przyjmij klasę podstawową jako parametr, wówczas MVC nie będzie w stanie utworzyć jej abstrakcyjnej wersji (której i tak nie chciałbym chcieć, chciałbym, aby tworzył rzeczywisty konkretny typ). Jeśli utworzę wiele metod działania po zmianie, które różnią się tylko sygnaturą parametru, wówczas MVC skarży się, że jest niejednoznaczne.

Tak dalece, jak mogę powiedzieć, mam kilka opcji, jak rozwiązać ten problem. Nie podoba mi się żadna z nich z różnych powodów, ale wymienię je tutaj:

  1. Utwórz niestandardowy segregator, jak sugeruje Darin w pierwszym wpisie, z którym się łączyłem.
  2. Utwórz atrybut dyskryminatora jako drugi post, który połączyłem z sugestiami.
  3. Opublikuj w różnych metodach działania na podstawie typu
  4. ???

Nie podoba mi się 1, ponieważ jest to w zasadzie konfiguracja, która jest ukryta. Inni deweloperzy pracujący nad kodem mogą o tym nie wiedzieć i tracić dużo czasu, próbując dowiedzieć się, dlaczego rzeczy się psują, gdy zmieniają się rzeczy.

Nie podoba mi się 2, ponieważ wydaje się dość hacky. Ale jestem skłonny do tego podejścia.

Nie podoba mi się 3, ponieważ oznacza to naruszenie DRY.

Jakieś inne sugestie?

Edit:

postanowiłem iść metodą Darin, ale zanotował nieznaczne zmiany. Dodałem to do mojego abstrakcyjnego modelu:

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

Następnie ukryty automatycznie generowany jest w moim DisplayForModel(). Trzeba tylko pamiętać, że jeśli nie używasz DisplayForModel(), musisz dodać to sam.

Odpowiedz

59

Odkąd oczywiście zdecydować się na opcję 1 (:-)) pozwól mi spróbować go rozwinąć trochę bardziej, że jest to mniej łamliwe i uniknąć hardcoding konkretne instancje do spoiwa modelu. Chodzi o to, aby przekazać konkretny typ do ukrytego pola i użyć odbicia, aby utworzyć konkretny typ.

Załóżmy, że masz następujące modele wyświetlania:

public abstract class BaseViewModel 
{ 
    public int Id { get; set; } 
} 

public class FooViewModel : BaseViewModel 
{ 
    public string Foo { get; set; } 
} 

następujący sterownik:

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

odpowiedni Index Widok:

@model BaseViewModel 
@using (Html.BeginForm()) 
{ 
    @Html.Hidden("ModelType", Model.GetType())  
    @Html.EditorForModel() 
    <input type="submit" value="OK" /> 
} 

i szablon ~/Views/Home/EditorTemplates/FooViewModel.cshtml redaktor:

@model FooViewModel 
@Html.EditorFor(x => x.Id) 
@Html.EditorFor(x => x.Foo) 

Teraz możemy mieć następujący zwyczaj modelu Spoiwo:

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

Rzeczywisty typ jest wywnioskować z wartości pola ukrytego ModelType. Nie jest to na stałe zakodowane, co oznacza, że ​​możesz później dodać inne typy dzieci bez konieczności dotykania tego segregatora.

Ta sama technika może być easily be applied do kolekcji modeli widoku podstawowego.

+0

Hmm .. To zdecydowanie poprawia łatwość konserwacji. I działałoby tam, gdzie twoje konkretne modele nie różnią się pod względem podpisów nieruchomości (wada w metodzie Attribute). Nie jestem pewien, czy lubię zanieczyszczać moje poglądy tymi rodzajami dyskryminatorów. –

+0

Wybrałem Twoje rozwiązanie, ale dokonałem niewielkiej zmiany. Zobacz moją edycję. –

+0

Znalazłem też inne rozwiązanie (patrz moja odpowiedź), które okazuje się dużo prostsze i nieco bezpieczniejsze, ponieważ nie wiąże się z ujawnieniem typowi danych klientowi i mając nadzieję, że nie było zhakowany przez hakera. –

4

Stosując metodę Darina do rozróżniania typów modeli za pomocą ukrytego pola w widoku, zaleca się użycie niestandardowego RouteHandler w celu rozróżnienia typów modeli i skierowania każdego do akcji o unikalnym nazwaniu na kontrolerze. Na przykład, jeśli masz dwa modele betonu, Foo i Bar, dla swojej akcji Create w kontrolerze, wykonaj akcję CreateFoo(Foo model) i akcję CreateBar(Bar model). Następnie dokonać zwyczaj RouteHandler, co następuje:

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

Następnie w Global.asax.cs zmień RegisterRoutes() następująco:

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

wtedy, gdy przychodzi Tworzenie żądania, jeśli ModelType jest zdefiniowany w zwracanym formularzu, funkcja RouteHandler doda parametr ModelType do nazwy działania, umożliwiając zdefiniowanie unikalnego działania dla każdego konkretnego modelu.

+0

Rozwiązania te zależą od posiadania ukrytego pola. Ale to ma dwa problemy. 1), że deweloper musi pamiętać o dodaniu ukrytego pola, i 2) możliwe jest, że ktoś mógłby stworzyć parametr modelu o tej samej nazwie i przypadkowo potknąłby się dziwny problem z routingiem. Czyniąc to globalnie, jest to przyjemne i wygodne, ale łatwe do złamania i/lub potknięcia się. Podejście atrybutów ma tę zaletę, że wpływa tylko na tę metodę działania. –

+0

Ciekawe podejście – BlackTigerX

14

Właśnie pomyślałem o ciekawym rozwiązaniu tego problemu. Zamiast używać parametrów bsed modelu wiązania tak:

[HttpPost] 
public ActionResult Index(MyModel model) {...} 

mogę zamiast używać TryUpdateModel(), aby pozwolić mi określić, jakie wiążą się z modelem w kodzie. Na przykład zrobić coś takiego:

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

To faktycznie działa dużo lepiej tak czy inaczej, bo jeśli robię żadnej obróbki, wówczas będę musiał rzucić model cokolwiek to rzeczywiście jest tak czy inaczej, lub użyć is aby znaleźć właściwą mapę do połączenia z AutoMapper.

Przypuszczam, że ci z nas, którzy nie używali MVC od 1 dnia, zapomnieli o UpdateModel i TryUpdateModel, ale nadal mają swoje zastosowania.

+1

Może zrobiłem coś nie tak, ale jeśli po prostu zrobię "TryUpdateModel (model)" zaktualizuje tylko właściwości itp. 'MyModel', Jeśli chcę to, aby w pełni zaktualizować model muszę zrobić' TryUpdateModel ((MyDerivedModel) model) ', ale tak czy inaczej, jest to świetna sztuczka. – Brook

7

Zajęło mi to dobry dzień, aby wymyślić odpowiedź na ściśle związany problem - chociaż nie jestem pewien, czy to dokładnie ten sam problem, zamieszczam go tutaj, na wypadek, gdyby inni szukali rozwiązania dla ten sam dokładny problem.

W moim przypadku mam abstrakcyjny typ bazowy dla wielu różnych typów modelu widoku. Więc w głównym widoku model, mam właściwość abstrakcyjnej podstawy typu:

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

Mam szereg podtypów AbstractBaseItemView, z których wiele definiować własne wyłączne właściwości.

Mój problem jest model-spoiwo nie wygląda na typ obiektu dołączonym do View.ItemView, lecz tylko wygląda na zadeklarowanym typie własności, która jest AbstractBaseItemView - i postanawia związać tylko właściwości zdefiniowane w typie abstrakcyjnym, ignorując właściwości specyficzne dla konkretnego typu AbstractBaseItemView, który jest w użyciu.

Prace wokół tego nie jest ładny:

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

    // ... 
} 

Chociaż ta zmiana czuje hacky i jest bardzo „układowe”, wydaje się do pracy - a nie, o ile mogę zrozumieć, stwarzają poważne zagrożenie dla bezpieczeństwa, ponieważ nie wiążą się z , a nie w CreateModel(), a zatem , a nie pozwalają na umieszczanie czegokolwiek i oszukiwanie modelu-spoiwa w tworzeniu dowolnego obiektu.

Działa również tylko wtedy, gdy zadeklarowany typ właściwości to typ abstrakcyjny , np., np. Klasa abstrakcyjna lub interfejs.

Na podobnej uwadze, wydaje mi się, że inne implementacje, które widziałem, które zastępują CreateModel() prawdopodobnie będą działać tylko wtedy, gdy publikujesz zupełnie nowe obiekty - i będą cierpieć z powodu tego samego problemu, który prowadziłem do, gdy zadeklarowany typ właściwości jest typu abstrakcyjnego. Najprawdopodobniej nie będziesz w stanie edytować określonych właściwości konkretnych typów obiektów na obiektach modelu , a jedynie tworzyć nowe.

Innymi słowy, prawdopodobnie będziesz musiał zintegrować to obejście ze swoim segregatorem, aby móc poprawnie edytować obiekty, które zostały dodane do modelu widoku przed wiązaniem ... Osobiście uważam, że to jest bezpieczniejsze podejście, ponieważ kontroluję, jaki konkretny typ zostanie dodany - aby kontroler/akcja mogła pośrednio określić konkretny typ, który może być związany, po prostu wypełniając właściwość pustą instancją.

Mam nadzieję, że jest to pomocne dla innych ...

+0

To zadziałało bardzo dobrze. Pracowałem przez kilka dni, aby znaleźć rozwiązanie. Udało mi się stworzyć nowe obiekty dokładnie tak, jak opisujesz, ale właściwości konkretnego typu nigdy nie zostały wypełnione spoiwem. Teraz są. – davcar

+0

Cieszę się, że to zadziałało dla kogoś - wygląda na to, że większość ludzi nie rozumie tego problemu na tyle, by nawet się tym przejmować ;-) –

Powiązane problemy