Wie ordne ich das Ansichtsmodell in einer POST-Aktion wieder dem Domänenmodell zu?

86

Jeder Artikel im Internet über die Verwendung von ViewModels und Automapper enthält die Richtlinien für die Richtungszuordnung "Controller -> Ansicht". Sie nehmen ein Domänenmodell zusammen mit allen Auswahllisten in ein spezielles ViewModel und übergeben es an die Ansicht. Das ist klar und gut.
Die Ansicht hat ein Formular, und schließlich befinden wir uns in der POST-Aktion. Hier kommen alle Modellbinder zusammen mit [offensichtlich] einem anderen Ansichtsmodell zur Szene, das [offensichtlich] zumindest im Teil der Namenskonventionen zum Zwecke der Bindung und Validierung mit dem ursprünglichen ViewModel verwandt ist .

Wie ordnen Sie es Ihrem Domain-Modell zu?

Lassen Sie es eine Einfügeaktion sein, wir könnten den gleichen Automapper verwenden. Aber was ist, wenn es sich um eine Update-Aktion handelt? Wir müssen unsere Domänenentität aus dem Repository abrufen, ihre Eigenschaften gemäß den Werten im ViewModel aktualisieren und im Repository speichern.

ADDENDUM 1 (9. Februar 2010): Manchmal reicht es nicht aus, die Eigenschaften des Modells zuzuweisen . Es sollten Maßnahmen gegen das Domänenmodell gemäß den Werten des Ansichtsmodells ergriffen werden. Das heißt, einige Methoden sollten für das Domänenmodell aufgerufen werden. Wahrscheinlich sollte es eine Art Anwendungsdienstschicht geben, die zwischen Controller und Domäne steht, um Ansichtsmodelle zu verarbeiten ...


Wie organisiere ich diesen Code und wo platziere ich ihn, um die folgenden Ziele zu erreichen?

  • Halten Sie die Controller dünn
  • Ehre SoC Praxis
  • Befolgen Sie die Prinzipien des domänengesteuerten Designs
  • trocken sein
  • Fortsetzung folgt ...
Anthony Serdyukov
quelle

Antworten:

37

Ich verwende eine IBuilder- Schnittstelle und implementiere sie mit dem ValueInjecter

public interface IBuilder<TEntity, TViewModel>
{
      TEntity BuildEntity(TViewModel viewModel);
      TViewModel BuildViewModel(TEntity entity);
      TViewModel RebuildViewModel(TViewModel viewModel); 
}

... (Implementierung) RebuildViewModel ruft nur aufBuildViewModel(BuilEntity(viewModel))

[HttpPost]
public ActionResult Update(ViewModel model)
{
   if(!ModelState.IsValid)
    {
       return View(builder.RebuildViewModel(model);
    }

   service.SaveOrUpdate(builder.BuildEntity(model));
   return RedirectToAction("Index");
}

Übrigens schreibe ich kein ViewModel Ich schreibe Input, weil es viel kürzer ist, aber das ist einfach nicht wirklich wichtig,
hoffe es hilft

Update: Ich verwende diesen Ansatz jetzt in der ProDinner ASP.net MVC-Demo-App . Er heißt jetzt IMapper. Außerdem wird ein PDF bereitgestellt, in dem dieser Ansatz ausführlich erläutert wird

Omu
quelle
Ich mag diesen Ansatz. Eine Sache, die mir jedoch nicht klar ist, ist die Implementierung von IBuilder, insbesondere im Hinblick auf eine abgestufte Anwendung. Zum Beispiel hat mein ViewModel 3 SelectLists. Wie ruft die Builder-Implementierung die Auswahllistenwerte aus dem Repository ab?
Matt Murrell
@ Matt Murrell Blick auf prodinner.codeplex.com Ich mache das dort und ich nenne es dort IMapper anstelle von IBuilder
Omu
6
Ich mag diesen Ansatz, ich habe hier ein Beispiel implementiert: gist.github.com/2379583
Paul Stovell
Meiner Meinung nach entspricht es nicht dem Domain Model-Ansatz. Es sieht aus wie ein CRUD-Ansatz für unklare Anforderungen. Sollten wir nicht Fabriken (DDD) und verwandte Methoden im Domänenmodell verwenden, um vernünftige Maßnahmen zu vermitteln? Auf diese Weise sollten wir eine Entität besser aus der Datenbank laden und nach Bedarf aktualisieren, oder? Es sieht also so aus, als ob es nicht ganz richtig ist.
Artyom
7

Tools wie AutoMapper können verwendet werden, um vorhandene Objekte mit Daten aus dem Quellobjekt zu aktualisieren. Die Controller-Aktion zum Aktualisieren könnte folgendermaßen aussehen:

[HttpPost]
public ActionResult Update(MyViewModel viewModel)
{
    MyDataModel dataModel = this.DataRepository.GetMyData(viewModel.Id);
    Mapper<MyViewModel, MyDataModel>(viewModel, dataModel);
    this.Repostitory.SaveMyData(dataModel);
    return View(viewModel);
}

Abgesehen von dem, was im obigen Ausschnitt zu sehen ist:

  • POST-Daten zum Anzeigen von Modell + Validierung werden in ModelBinder durchgeführt (können mit benutzerdefinierten Bindungen erweitert werden).
  • Die Fehlerbehandlung (dh das Abfangen von Datenzugriffsausnahme-Auslösungen durch das Repository) kann durch den Filter [HandleError] erfolgen

Die Controller-Aktion ist ziemlich dünn und die Bedenken sind getrennt: Zuordnungsprobleme werden in der AutoMapper-Konfiguration behoben, die Validierung erfolgt durch ModelBinder und der Datenzugriff durch Repository.

PanJanek
quelle
6
Ich bin mir nicht sicher, ob Automapper hier nützlich ist, da es die Abflachung nicht rückgängig machen kann. Schließlich ist das Domänenmodell kein einfaches DTO wie das Ansichtsmodell, daher reicht es möglicherweise nicht aus, ihm einige Eigenschaften zuzuweisen. Wahrscheinlich sollten einige Aktionen für das Domänenmodell gemäß dem Inhalt des Ansichtsmodells ausgeführt werden. Allerdings +1 für das Teilen eines recht guten Ansatzes.
Anthony Serdyukov
@ Anton ValueInjecter kann Abflachung umkehren;)
Omu
Mit diesem Ansatz halten Sie den Controller nicht dünn, Sie verletzen SoC und DRY ... wie Omu erwähnt hat, sollten Sie eine separate Ebene haben, die sich um das Mapping kümmert.
Rookian
5

Ich möchte sagen, dass Sie den Begriff ViewModel für beide Richtungen der Client-Interaktion wiederverwenden. Wenn Sie genug ASP.NET MVC-Code in freier Wildbahn gelesen haben, haben Sie wahrscheinlich den Unterschied zwischen einem ViewModel und einem EditModel gesehen. Ich denke das ist wichtig.

Ein ViewModel repräsentiert alle Informationen, die zum Rendern einer Ansicht erforderlich sind. Dies kann Daten umfassen, die an statischen, nicht interaktiven Orten gerendert werden, sowie Daten, die lediglich eine Überprüfung durchführen, um zu entscheiden, was genau gerendert werden soll. Eine Controller-GET-Aktion ist im Allgemeinen dafür verantwortlich, das ViewModel für seine Ansicht zu verpacken.

Ein EditModel (oder möglicherweise ein ActionModel) stellt die Daten dar, die erforderlich sind, um die Aktion auszuführen, die der Benutzer für diesen POST ausführen wollte. Ein EditModel versucht also wirklich, eine Aktion zu beschreiben. Dies wird wahrscheinlich einige Daten aus dem ViewModel ausschließen, und obwohl sie verwandt sind, denke ich, ist es wichtig zu erkennen, dass sie tatsächlich unterschiedlich sind.

Eine Idee

Das heißt, Sie könnten sehr leicht eine AutoMapper-Konfiguration haben, um von Modell -> ViewModel zu wechseln, und eine andere, um von EditModel -> Modell zu wechseln. Dann müssen die verschiedenen Controller-Aktionen nur noch AutoMapper verwenden. Zum Teufel könnte das EditModel eine Funktion haben, um seine Eigenschaften anhand des Modells zu validieren und diese Werte auf das Modell selbst anzuwenden. Es macht nichts anderes und Sie haben ModelBinders in MVC, um die Anforderung trotzdem dem EditModel zuzuordnen.

Eine andere Idee

Darüber hinaus habe ich in letzter Zeit darüber nachgedacht, dass die Idee eines ActionModels darin besteht, dass der Client Ihnen tatsächlich mehrere Aktionen beschreibt, die der Benutzer ausgeführt hat, und nicht nur einen großen Datenglob. Dies würde sicherlich etwas Javascript auf der Clientseite erfordern, um es zu verwalten, aber die Idee ist meiner Meinung nach faszinierend.

Wenn der Benutzer Aktionen auf dem von Ihnen dargestellten Bildschirm ausführt, erstellt Javascript im Wesentlichen eine Liste von Aktionsobjekten. Ein Beispiel ist möglicherweise, dass sich der Benutzer auf einem Mitarbeiterinformationsbildschirm befindet. Sie aktualisieren den Nachnamen und fügen eine neue Adresse hinzu, da der Mitarbeiter kürzlich verheiratet war. Unter der Decke erzeugt dies ein ChangeEmployeeNameund ein AddEmployeeMailingAddressObjekt zu einer Liste. Der Benutzer klickt auf "Speichern", um die Änderungen zu übernehmen, und Sie senden die Liste von zwei Objekten, die jeweils nur die Informationen enthalten, die zum Ausführen jeder Aktion erforderlich sind.

Sie benötigen einen intelligenteren ModelBinder als den Standard, aber ein guter JSON-Serializer sollte sich um die Zuordnung der clientseitigen Aktionsobjekte zu den serverseitigen Objekten kümmern können. Die serverseitigen (wenn Sie sich in einer zweistufigen Umgebung befinden) verfügen möglicherweise über Methoden, mit denen die Aktion für das Modell ausgeführt wird, mit dem sie arbeiten. Die Controller-Aktion erhält also nur eine ID für die Modellinstanz zum Abrufen und eine Liste der Aktionen, die für sie ausgeführt werden sollen. Oder die Aktionen enthalten die ID, um sie sehr getrennt zu halten.

Vielleicht wird so etwas auf der Serverseite realisiert:

public interface IUserAction<TModel>
{
     long ModelId { get; set; }
     IEnumerable<string> Validate(TModel model);
     void Complete(TModel model);
}

[Transaction] //just assuming some sort of 2-tier with transactions handled by filter
public ActionResult Save(IEnumerable<IUserAction<Employee>> actions)
{
     var errors = new List<string>();
     foreach( var action in actions ) 
     {
         // relying on ORM's identity map to prevent multiple database hits
         var employee = _employeeRepository.Get(action.ModelId);
         errors.AddRange(action.Validate(employee));
     }

     // handle error cases possibly rendering view with them

     foreach( var action in editModel.UserActions )
     {
         var employee = _employeeRepository.Get(action.ModelId);
         action.Complete(employee);
         // against relying on ORMs ability to properly generate SQL and batch changes
         _employeeRepository.Update(employee);
     }

     // render the success view
}

Das macht die Aktion zum Zurücksenden wirklich ziemlich allgemein, da Sie sich auf Ihren ModelBinder verlassen, um die richtige IUserAction-Instanz zu erhalten, und Ihre IUserAction-Instanz, um entweder die richtige Logik selbst auszuführen oder (wahrscheinlicher) das Modell mit den Informationen aufzurufen.

Wenn Sie sich in einer dreistufigen Umgebung befinden, kann die IUserAction einfach zu einfachen DTOs gemacht werden, die über die Grenze geschossen und auf ähnliche Weise auf der App-Ebene ausgeführt werden. Abhängig davon, wie Sie diese Ebene erstellen, kann sie sehr einfach aufgeteilt werden und dennoch in einer Transaktion verbleiben (was in den Sinn kommt, ist die Anfrage / Antwort von Agatha und die Nutzung der Identitätskarte von DI und NHibernate).

Ich bin mir jedenfalls sicher, dass dies keine perfekte Idee ist. Es würde einige JS auf Client-Seite erfordern, um sie zu verwalten, und ich konnte noch kein Projekt durchführen, um zu sehen, wie es sich entwickelt, aber der Beitrag versuchte darüber nachzudenken, wie Komm hin und zurück, also dachte ich mir, ich würde meine Gedanken geben. Ich hoffe es hilft und ich würde gerne von anderen Möglichkeiten hören, die Interaktionen zu verwalten.

Sean Copenhaver
quelle
Interessant. In Bezug auf die Unterscheidung zwischen ViewModel und EditModel ... schlagen Sie vor, dass Sie für eine Bearbeitungsfunktion ein ViewModel verwenden würden, um das Formular zu erstellen, und es dann an ein EditModel binden würden, wenn der Benutzer es veröffentlicht hat? Wenn ja, wie würden Sie mit Situationen umgehen, in denen Sie das Formular aufgrund von Validierungsfehlern erneut veröffentlichen müssten (z. B. wenn das ViewModel Elemente zum Auffüllen eines Dropdowns enthielt) - würden Sie auch nur die Dropdown-Elemente in das EditModel aufnehmen? In welchem ​​Fall wäre der Unterschied zwischen den beiden?
UpTheCreek
Ich vermute, Sie befürchten, dass ich mein ViewModel neu erstellen muss, wenn ich ein EditModel verwende und ein Fehler auftritt, was sehr teuer sein kann. Ich würde sagen, erstellen Sie einfach das ViewModel neu und stellen Sie sicher, dass dort Benutzerbenachrichtigungen (wahrscheinlich sowohl positive als auch negative wie Validierungsfehler) abgelegt werden können. Wenn sich herausstellt, dass es sich um ein Leistungsproblem handelt, können Sie das ViewModel jederzeit zwischenspeichern, bis die nächste Anforderung dieser Sitzung beendet ist (wahrscheinlich der Beitrag des EditModel).
Sean Copenhaver