asp.net mvc - Polymorphe Modellbindung

Translate

Diese Frage wurde gestelltvorher gefragtin früheren Versionen von MVC. Es gibt auchdieser Blogeintragüber einen Weg, um das Problem zu umgehen. Ich frage mich, ob MVC3 etwas eingeführt hat, das helfen könnte, oder ob es andere Optionen gibt.

In einer Nussschale. Hier ist die Situation. Ich habe ein abstraktes Basismodell und 2 konkrete Unterklassen. Ich habe eine stark typisierte Ansicht, mit der die Modelle gerendert werdenEditorForModel(). Dann habe ich benutzerdefinierte Vorlagen, um jeden konkreten Typ zu rendern.

Das Problem tritt nach der Zeit auf. Wenn ich die Post-Action-Methode dazu bringe, die Basisklasse als Parameter zu verwenden, kann MVC keine abstrakte Version davon erstellen (was ich sowieso nicht möchte, ich möchte, dass es den tatsächlichen konkreten Typ erstellt). Wenn ich mehrere Post-Action-Methoden erstelle, die nur durch die Parametersignatur variieren, beschwert sich MVC, dass dies nicht eindeutig ist.

Soweit ich das beurteilen kann, habe ich einige Möglichkeiten, wie ich dieses Problem lösen kann. Ich mag keinen von ihnen aus verschiedenen Gründen, aber ich werde sie hier auflisten:

  1. Erstellen Sie einen benutzerdefinierten Modellordner, wie Darin im ersten Beitrag, auf den ich verlinkt habe, vorschlägt.
  2. Erstellen Sie ein Diskriminatorattribut, wie der zweite Beitrag, auf den ich verlinkt habe, vorschlägt.
  3. Posten Sie je nach Typ auf verschiedenen Aktionsmethoden
  4. ???

Ich mag 1 nicht, weil im Grunde die Konfiguration verborgen ist. Einige andere Entwickler, die an dem Code arbeiten, wissen möglicherweise nichts darüber und verschwenden viel Zeit damit, herauszufinden, warum Dinge kaputt gehen, wenn sich etwas ändert.

Ich mag 2 nicht, weil es irgendwie hackig erscheint. Aber ich neige zu diesem Ansatz.

Ich mag 3 nicht, weil das bedeutet, DRY zu verletzen.

Irgendwelche anderen Vorschläge?

Bearbeiten:

Ich entschied mich für Darins Methode, nahm aber eine kleine Änderung vor. Ich habe dies meinem abstrakten Modell hinzugefügt:

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

Dann wird automatisch ein verstecktes in meinem generiertDisplayForModel(). Das einzige, woran Sie sich erinnern müssen, ist, dass Sie es nicht verwendenDisplayForModel()müssen Sie es selbst hinzufügen.

This question and all comments follow the "Attribution Required."

Alle Antworten

Translate

Da ich mich offensichtlich für Option 1 (:-)) entscheide, möchte ich versuchen, es etwas genauer zu erläutern, damit es weniger istzerbrechlichund vermeiden Sie das Festcodieren konkreter Instanzen in den Modellordner. Die Idee ist, den Betontyp in ein verborgenes Feld zu leiten und den Betontyp mithilfe von Reflexion zu instanziieren.

Angenommen, Sie haben die folgenden Ansichtsmodelle:

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

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

die folgende Steuerung:

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

die entsprechendeIndexAussicht:

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

und die~/Views/Home/EditorTemplates/FooViewModel.cshtmlEditor-Vorlage:

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

Jetzt könnten wir den folgenden benutzerdefinierten Modellordner haben:

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

Der tatsächliche Typ wird aus dem Wert von abgeleitetModelTypeverstecktes Feld. Es ist nicht fest codiert, was bedeutet, dass Sie später weitere untergeordnete Typen hinzufügen können, ohne diesen Modellordner jemals berühren zu müssen.

Die gleiche Technik könnte seinleicht angewendet werdenzu Sammlungen von Basisansichtsmodellen.

Quelle
Translate

Ich habe gerade an eine interessante Lösung für dieses Problem gedacht. Anstatt die Parameter-bsed-Modellbindung wie folgt zu verwenden:

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

Ich kann stattdessen TryUpdateModel () verwenden, um zu bestimmen, an welche Art von Modell im Code gebunden werden soll. Zum Beispiel mache ich so etwas:

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

Das funktioniert sowieso viel besser, denn wenn ich eine Verarbeitung mache, müsste ich das Modell sowieso auf das umwandeln, was es tatsächlich ist, oder es verwendenisum herauszufinden, welche Karte mit AutoMapper aufgerufen werden soll.

Ich denke, diejenigen von uns, die MVC seit Tag 1 nicht mehr verwendet haben, vergessen esUpdateModelundTryUpdateModel, aber es hat immer noch seine Verwendung.

Quelle
Translate

Ich habe einen guten Tag gebraucht, um eine Antwort auf ein eng verwandtes Problem zu finden - obwohl ich nicht sicher bin, ob es genau dasselbe Problem ist, poste ich es hier, falls andere nach einer Lösung für genau dasselbe Problem suchen.

In meinem Fall habe ich einen abstrakten Basistyp für eine Reihe verschiedener Ansichtsmodelltypen. Im Hauptansichtsmodell habe ich also eine Eigenschaft eines abstrakten Basistyps:

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

Ich habe eine Reihe von Untertypen von AbstractBaseItemView, von denen viele ihre eigenen exklusiven Eigenschaften definieren.

Mein Problem ist, dass der Modellbinder nicht den Objekttyp betrachtet, der an View.ItemView angehängt ist, sondern nur den deklarierten Eigenschaftstyp, AbstractBaseItemView - und sich für die Bindung entscheidetnurdie im abstrakten Typ definierten Eigenschaften, wobei Eigenschaften ignoriert werden, die für den konkreten Typ von AbstractBaseItemView spezifisch sind, der gerade verwendet wird.

Die Umgehung dafür ist nicht schön:

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

    // ...
}

Obwohl sich diese Änderung hackig anfühlt und sehr "systemisch" ist, scheint sie zu funktionieren - und stellt meines Erachtens kein erhebliches Sicherheitsrisiko dar, da dies der Fall istnichtbinde an CreateModel () und tue dies auchnichtSie können alles posten und den Modellbinder dazu bringen, nur ein beliebiges Objekt zu erstellen.

Es funktioniert auch nur, wenn der deklarierte Eigenschaftstyp ein istabstraktTyp, z. B. eine abstrakte Klasse oder eine Schnittstelle.

In einem ähnlichen Zusammenhang fällt mir ein, dass andere Implementierungen, die ich hier gesehen habe und die CreateModel () überschreiben, dies wahrscheinlich tun werdennurfunktionieren, wenn Sie völlig neue Objekte veröffentlichen - und unter dem gleichen Problem leiden, auf das ich gestoßen bin, wenn der deklarierte Eigenschaftstyp ein abstrakter Typ ist. Sie werden es also höchstwahrscheinlich nicht könnenbearbeitenspezifische Eigenschaften von Betontypen aufbestehenderObjekte modellieren, aber nur neue erstellen.

Mit anderen Worten, Sie müssen diese Problemumgehung wahrscheinlich in Ihren Ordner integrieren, um auch Objekte, die vor dem Binden zum Ansichtsmodell hinzugefügt wurden, ordnungsgemäß bearbeiten zu können. Ich persönlich halte dies für einen sichereren Ansatz, da Ich steuere, welcher konkrete Typ hinzugefügt wird - so kann der Controller / die Aktion indirekt den konkreten Typ angeben, der gebunden werden kann, indem er die Eigenschaft einfach mit einer leeren Instanz füllt.

Ich hoffe das ist hilfreich für andere ...

Quelle
Translate

Wenn Sie Darins Methode verwenden, um Ihre Modelltypen über ein verstecktes Feld in Ihrer Ansicht zu unterscheiden, würde ich empfehlen, dass Sie eine benutzerdefinierte Methode verwendenRouteHandlerum Ihre Modelltypen zu unterscheiden und jeden auf eine eindeutig benannte Aktion auf Ihrem Controller zu verweisen. Zum Beispiel, wenn Sie zwei konkrete Modelle haben, Foo und Bar, für IhreCreateAktion in Ihrem Controller, machen Sie eineCreateFoo(Foo model)Aktion und aCreateBar(Bar model)Aktion. Erstellen Sie dann einen benutzerdefinierten RouteHandler wie folgt:

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

Ändern Sie dann in Global.asax.csRegisterRoutes()wie folgt:

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

Wenn dann eine Erstellungsanforderung eingeht und ein ModelType im zurückgegebenen Formular definiert ist, hängt der RouteHandler den ModelType an den Aktionsnamen an, sodass für jedes konkrete Modell eine eindeutige Aktion definiert werden kann.

Quelle
Leave a Reply
You must be logged in to post a answer.
Über den Autor