Best Practices für ViewModel

238

Aus dieser Frage geht hervor , dass es sinnvoll ist, einen Controller ein ViewModel erstellen zu lassen , das das Modell, das in der Ansicht angezeigt werden soll, genauer wiedergibt. Ich bin jedoch neugierig auf einige der Konventionen (ich bin neu im MVC-Muster) , wenn es nicht schon offensichtlich war).

Grundsätzlich hatte ich folgende Fragen:

  1. Normalerweise möchte ich eine Klasse / Datei haben. Ist dies bei einem ViewModel sinnvoll, wenn es nur erstellt wird, um Daten von einem Controller an eine Ansicht zu übergeben?
  2. Wenn ein ViewModel in eine eigene Datei gehört und Sie eine Verzeichnis- / Projektstruktur verwenden, um die Dinge getrennt zu halten, wohin gehört die ViewModel- Datei? Im Controller- Verzeichnis?

Das ist es im Grunde für jetzt. Ich habe vielleicht noch ein paar Fragen, aber das hat mich in der letzten Stunde oder so gestört, und ich kann anscheinend anderswo konsequente Anleitungen finden.

BEARBEITEN: Wenn ich mir die Beispiel- NerdDinner-App auf CodePlex ansehe , sieht es so aus, als ob die ViewModels Teil der Controller sind , aber es ist mir trotzdem unangenehm, dass sie nicht in ihren eigenen Dateien enthalten sind.

jerhinesmith
quelle
66
Ich würde NerdDinner nicht als "Best Practices" -Beispiel bezeichnen. Ihre Intuition dient Ihnen gut. :)
Ryan Montgomery

Antworten:

211

Ich erstelle für jede Ansicht ein sogenanntes "ViewModel". Ich habe sie in einem Ordner namens ViewModels in meinem MVC-Webprojekt abgelegt. Ich benenne sie nach dem Controller und der Aktion (oder Ansicht), die sie darstellen. Wenn ich also Daten an die SignUp-Ansicht auf dem Membership Controller übergeben muss, erstelle ich eine MembershipSignUpViewModel.cs-Klasse und lege sie in den ViewModels-Ordner.

Dann füge ich die notwendigen Eigenschaften und Methoden hinzu, um die Übertragung von Daten vom Controller zur Ansicht zu erleichtern. Ich verwende den Automapper, um von meinem ViewModel zum Domänenmodell und bei Bedarf wieder zurück zu gelangen.

Dies funktioniert auch gut für zusammengesetzte ViewModels, die Eigenschaften enthalten, die vom Typ anderer ViewModels sind. Wenn Sie beispielsweise 5 Widgets auf der Indexseite im Mitgliedschafts-Controller haben und für jede Teilansicht ein ViewModel erstellt haben, wie übergeben Sie die Daten aus der Index-Aktion an die Partials? Sie fügen dem MembershipIndexViewModel eine Eigenschaft vom Typ MyPartialViewModel hinzu, und beim Rendern des Teils würden Sie Model.MyPartialViewModel übergeben.

Auf diese Weise können Sie die partiellen ViewModel-Eigenschaften anpassen, ohne die Indexansicht ändern zu müssen. Es wird immer noch nur in Model.MyPartialViewModel übergeben, sodass die Wahrscheinlichkeit geringer ist, dass Sie die gesamte Kette von Partials durchlaufen müssen, um etwas zu reparieren, wenn Sie dem partiellen ViewModel lediglich eine Eigenschaft hinzufügen.

Ich werde auch den Namespace "MyProject.Web.ViewModels" zur web.config hinzufügen, damit ich sie in jeder Ansicht referenzieren kann, ohne jemals eine explizite Importanweisung für jede Ansicht hinzuzufügen. Macht es nur ein bisschen sauberer.

Ryan Montgomery
quelle
3
Was ist, wenn Sie aus einer Teilansicht POSTEN und die gesamte Ansicht zurückgeben möchten (im Falle eines Modellfehlers)? In der Teilansicht haben Sie keinen Zugriff auf das übergeordnete Modell.
Cosmo
5
@Cosmo: POST dann zu einer Aktion , die im Falle eines Modellfehlers die gesamte Ansicht zurückgeben kann. Auf der Serverseite haben Sie genug, um das übergeordnete Modell neu zu erstellen.
Tomas Aschan
Was ist mit Anmelde- [POST] und Anmeldeaktionen [GET]? mit verschiedenen Ansichtsmodellen?
Bart Calixto
Normalerweise ruft Login [GET] ViewModel nicht auf, da keine Daten geladen werden müssen.
Andre Figueiredo
Guter Hinweis. Wohin sollen Datenzugriff, Verarbeitung und Einstellung von Modell- / VM-Eigenschaften gehen? In meinem Fall stammen einige Daten aus einer lokalen CMS-Datenbank und einige aus Webdiensten, die verarbeitet / bearbeitet werden müssen, bevor sie für ein Modell festgelegt werden. Das alles in den Controller zu stecken wird ziemlich chaotisch.
xr280xr
124

Das Trennen von Klassen nach Kategorien (Controller, ViewModels, Filter usw.) ist Unsinn.

Wenn Sie Code für den Home-Bereich Ihrer Website (/) schreiben möchten, erstellen Sie einen Ordner mit dem Namen Home und legen Sie dort den HomeController, IndexViewModel, AboutViewModel usw. und alle zugehörigen Klassen ab, die von Home-Aktionen verwendet werden.

Wenn Sie gemeinsam genutzte Klassen wie einen ApplicationController haben, können Sie diese im Stammverzeichnis Ihres Projekts ablegen.

Warum verwandte Dinge trennen (HomeController, IndexViewModel) und Dinge zusammenhalten, die überhaupt keine Beziehung haben (HomeController, AccountController)?


Ich habe einen Blog-Beitrag zu diesem Thema geschrieben.

Max Toro
quelle
13
Wenn Sie dies tun, werden die Dinge ziemlich schnell ziemlich chaotisch.
UpTheCreek
14
Nein, chaotisch ist es, alle Controller in einem Verzeichnis / Namespace zu platzieren. Wenn Sie 5 Controller haben, die jeweils 5 Ansichtsmodelle verwenden, haben Sie 25 Ansichtsmodelle. Namespaces sind der Mechanismus zum Organisieren von Code und sollten hier nicht anders sein.
Max Toro
41
@ Max Toro: überrascht, dass du so sehr herabgestimmt wurdest. Nach einiger Zeit bei ASP.Net MVC habe ich große Schmerzen, wenn ich alle ViewModels an einem Ort, alle Controller an einem anderen und alle Views an einem anderen Ort habe. MVC ist ein Trio verwandter Stücke, sie sind gekoppelt - sie unterstützen sich gegenseitig. Ich denke, eine Lösung kann mich viel besser organisieren, wenn der Controller, ViewModels und Views für einen bestimmten Abschnitt zusammen im selben Verzeichnis leben. MyApp / Accounts / Controller.cs, MyApp / Accounts / Create / ViewModel.cs, MyApp / Accounts / Create / View.cshtml usw.
Quentin-Starin
13
@ RyanJMcGowan Trennung von Bedenken ist keine Trennung von Klassen.
Max Toro
12
@RyanJMcGowan Egal wie Sie sich der Entwicklung nähern, das Problem ist das, was Sie am Ende haben, insbesondere für große Apps. Sobald Sie im Wartungsmodus sind, denken Sie nicht an alle Modelle, sondern an alle Steuerungen. Sie fügen jeweils eine Funktion hinzu.
Max Toro
21

Ich behalte meine Anwendungsklassen in einem Unterordner namens "Core" (oder einer separaten Klassenbibliothek) und verwende dieselben Methoden wie die KIGG- Beispielanwendung, jedoch mit einigen geringfügigen Änderungen, um meine Anwendungen trockener zu machen.

Ich erstelle eine BaseViewData-Klasse in / Core / ViewData /, in der ich allgemeine standortweite Eigenschaften speichere.

Danach erstelle ich alle meine ViewData-Klassen in demselben Ordner, die dann von BaseViewData abgeleitet sind und ansichtsspezifische Eigenschaften haben.

Dann erstelle ich einen ApplicationController, von dem alle meine Controller abgeleitet sind. Der ApplicationController verfügt über eine generische GetViewData-Methode wie folgt:

protected T GetViewData<T>() where T : BaseViewData, new()
    {
        var viewData = new T
        {
           Property1 = "value1",
           Property2 = this.Method() // in the ApplicationController
        };
        return viewData;
    }

Schließlich mache ich in meiner Controller-Aktion Folgendes, um mein ViewData-Modell zu erstellen

public ActionResult Index(int? id)
    {
        var viewData = this.GetViewData<PageViewData>();
        viewData.Page = this.DataContext.getPage(id); // ApplicationController
        ViewData.Model = viewData;
        return View();
    }

Ich denke, das funktioniert wirklich gut und hält Ihre Ansichten aufgeräumt und Ihre Controller dünn.

Kennzeichen
quelle
13

Eine ViewModel-Klasse dient dazu, mehrere durch Instanzen von Klassen dargestellte Daten in ein einfach zu verwaltendes Objekt zu kapseln, das Sie an Ihre Ansicht übergeben können.

Es wäre sinnvoll, Ihre ViewModel-Klassen in ihren eigenen Dateien im eigenen Verzeichnis zu haben. In meinen Projekten habe ich einen Unterordner des Ordners Models namens ViewModels. Dort leben meine ViewModels (zB ProductViewModel.cs).

JMS
quelle
13

Es gibt keinen guten Ort, an dem Sie Ihre Modelle aufbewahren können. Sie können sie in einer separaten Baugruppe aufbewahren, wenn das Projekt groß ist und viele ViewModels (Datenübertragungsobjekte) vorhanden sind. Sie können sie auch in einem separaten Ordner des Site-Projekts aufbewahren. In Oxite werden sie beispielsweise in einem Oxite-Projekt platziert, das auch viele verschiedene Klassen enthält. Controller in Oxite werden in ein separates Projekt verschoben, und Ansichten befinden sich ebenfalls in einem separaten Projekt.
In CodeCampServer heißen ViewModels * Form und werden im UI-Projekt im Ordner Models abgelegt.
Im MvcPress- Projekt werden sie im Datenprojekt platziert, das auch den gesamten Code für die Arbeit mit der Datenbank und ein bisschen mehr enthält (aber ich habe diesen Ansatz nicht empfohlen, er dient nur als Beispiel).
Sie sehen also, dass es viele Gesichtspunkte gibt. Normalerweise behalte ich meine ViewModels (DTO-Objekte) im Site-Projekt. Aber wenn ich mehr als 10 Modelle habe, ziehe ich es vor, sie in eine separate Baugruppe zu verschieben. Normalerweise verschiebe ich in diesem Fall auch die Steuerungen, um die Baugruppe zu trennen.
Eine andere Frage ist, wie Sie alle Daten vom Modell einfach Ihrem ViewModel zuordnen können. Ich schlage vor, einen Blick auf die AutoMapper- Bibliothek zu werfen . Ich mag es sehr, es erledigt alles Drecksarbeit für mich.
Außerdem schlage ich vor, das SharpArchitecture- Projekt zu betrachten. Es bietet eine sehr gute Architektur für Projekte und enthält viele coole Frameworks und Anleitungen sowie eine großartige Community.

zihotki
quelle
8
ViewModels! = DTO
Bart Calixto
6

Hier ist ein Code-Ausschnitt aus meinen Best Practices:

    public class UserController : Controller
    {
        private readonly IUserService userService;
        private readonly IBuilder<User, UserCreateInput> createBuilder;
        private readonly IBuilder<User, UserEditInput> editBuilder;

        public UserController(IUserService userService, IBuilder<User, UserCreateInput> createBuilder, IBuilder<User, UserEditInput> editBuilder)
        {
            this.userService = userService;
            this.editBuilder = editBuilder;
            this.createBuilder = createBuilder;
        }

        public ActionResult Index(int? page)
        {
            return View(userService.GetPage(page ?? 1, 5));
        }

        public ActionResult Create()
        {
            return View(createBuilder.BuildInput(new User()));
        }

        [HttpPost]
        public ActionResult Create(UserCreateInput input)
        {
            if (input.Roles == null) ModelState.AddModelError("roles", "selectati macar un rol");

            if (!ModelState.IsValid)
                return View(createBuilder.RebuildInput(input));

            userService.Create(createBuilder.BuilEntity(input));
            return RedirectToAction("Index");
        }

        public ActionResult Edit(long id)
        {
            return View(editBuilder.BuildInput(userService.GetFull(id)));
        }

        [HttpPost]
        public ActionResult Edit(UserEditInput input)
        {           
            if (!ModelState.IsValid)
                return View(editBuilder.RebuildInput(input));

            userService.Save(editBuilder.BuilEntity(input));
            return RedirectToAction("Index");
        }
}
Omu
quelle
5

Wir werfen alle unsere ViewModels in den Ordner Models (unsere gesamte Geschäftslogik befindet sich in einem separaten ServiceLayer-Projekt).

Piep Piep
quelle
4

Persönlich würde ich vorschlagen, wenn das ViewModel alles andere als trivial ist, dann verwenden Sie eine separate Klasse.

Wenn Sie mehr als ein Ansichtsmodell haben, ist es sinnvoll, es in mindestens ein Verzeichnis zu partitionieren. Wenn das Ansichtsmodell später freigegeben wird, erleichtert der im Verzeichnis enthaltene Namensraum das Verschieben in eine neue Assembly.

Preet Sangha
quelle
2

In unserem Fall haben wir die Modelle zusammen mit den Controllern in einem Projekt, das von den Ansichten getrennt ist.

Als Faustregel haben wir versucht, die meisten ["..."] ViewData-Inhalte in das ViewModel zu verschieben und zu vermeiden. Daher vermeiden wir Castings und magische Zeichenfolgen, was eine gute Sache ist.

Das ViewModel enthält auch einige allgemeine Eigenschaften wie Paginierungsinformationen für Listen oder Kopfzeileninformationen der Seite zum Zeichnen von Breadcrumbs und Titeln. In diesem Moment enthält die Basisklasse meiner Meinung nach zu viele Informationen, und wir können sie in drei Teile teilen, die grundlegendsten und notwendigsten Informationen für 99% der Seiten eines Basisansichtsmodells und dann ein Modell für die Listen und ein Modell für die Formulare, die bestimmte Daten für diese Szenarien enthalten und von der Basis erben.

Schließlich implementieren wir für jede Entität ein Ansichtsmodell, um mit den spezifischen Informationen umzugehen.

Marc Climent
quelle
0

Code in der Steuerung:

    [HttpGet]
        public ActionResult EntryEdit(int? entryId)
        {
            ViewData["BodyClass"] = "page-entryEdit";
            EntryEditViewModel viewMode = new EntryEditViewModel(entryId);
            return View(viewMode);
        }

    [HttpPost]
    public ActionResult EntryEdit(Entry entry)
    {
        ViewData["BodyClass"] = "page-entryEdit";            

        #region save

        if (ModelState.IsValid)
        {
            if (EntryManager.Update(entry) == 1)
            {
                return RedirectToAction("EntryEditSuccess", "Dictionary");
            }
            else
            {
                return RedirectToAction("EntryEditFailed", "Dictionary");
            }
        }
        else
        {
            EntryEditViewModel viewModel = new EntryEditViewModel(entry);
            return View(viewModel);
        }

        #endregion
    }

Code im Ansichtsmodell:

public class EntryEditViewModel
    {
        #region Private Variables for Properties

        private Entry _entry = new Entry();
        private StatusList _statusList = new StatusList();        

        #endregion

        #region Public Properties

        public Entry Entry
        {
            get { return _entry; }
            set { _entry = value; }
        }

        public StatusList StatusList
        {
            get { return _statusList; }
        }

        #endregion

        #region constructor(s)

        /// <summary>
        /// for Get action
        /// </summary>
        /// <param name="entryId"></param>
        public EntryEditViewModel(int? entryId)
        {
            this.Entry = EntryManager.GetDetail(entryId.Value);                 
        }

        /// <summary>
        /// for Post action
        /// </summary>
        /// <param name="entry"></param>
        public EntryEditViewModel(Entry entry)
        {
            this.Entry = entry;
        }

        #endregion       
    }

Projekte:

  • DevJet.Web (das ASP.NET MVC-Webprojekt)

  • DevJet.Web.App.Dictionary (ein separates Klassenbibliotheksprojekt)

    In diesem Projekt habe ich einige Ordner erstellt wie: DAL, BLL, BO, VM (Ordner für Ansichtsmodelle)

CT
quelle
Hallo, können Sie uns mitteilen, wie die Entry-Klasse aufgebaut ist?
Dinis Cruz
0

Erstellen Sie eine Basisklasse für das Ansichtsmodell, für die häufig erforderliche Eigenschaften wie das Ergebnis der Operation und Kontextdaten erforderlich sind. Sie können auch aktuelle Benutzerdaten und Rollen einfügen

class ViewModelBase 
{
  public bool HasError {get;set;} 
  public string ErrorMessage {get;set;}
  public List<string> UserRoles{get;set;}
}

In der Basis-Controller-Klasse haben Sie eine Methode wie PopulateViewModelBase (). Diese Methode füllt die Kontextdaten und Benutzerrollen. Mit HasError und ErrorMessage werden diese Eigenschaften festgelegt, wenn beim Abrufen von Daten aus service / db eine Ausnahme auftritt. Binden Sie diese Eigenschaften in der Ansicht, um Fehler anzuzeigen. Benutzerrollen können verwendet werden, um den ausgeblendeten Abschnitt in der Ansicht basierend auf Rollen anzuzeigen.

Um Ansichtsmodelle in verschiedenen Abrufaktionen zu füllen, kann sie konsistent gemacht werden, indem der Basiscontroller mit der abstrakten Methode FillModel verwendet wird

class BaseController :BaseController 
{
   public PopulateViewModelBase(ViewModelBase model) 
{
   //fill up common data. 
}
abstract ViewModelBase FillModel();
}

In Steuerungen

class MyController :Controller 
{

 public ActionResult Index() 
{
   return View(FillModel()); 
}

ViewModelBase FillModel() 
{ 
    ViewModelBase  model=;
    string currentAction = HttpContext.Current.Request.RequestContext.RouteData.Values["action"].ToString(); 
 try 
{ 
   switch(currentAction) 
{  
   case "Index": 
   model= GetCustomerData(); 
   break;
   // fill model logic for other actions 
}
}
catch(Exception ex) 
{
   model.HasError=true;
   model.ErrorMessage=ex.Message;
}
//fill common properties 
base.PopulateViewModelBase(model);
return model;
}
}
Ajay Kelkar
quelle