Best practice per ViewModel


238

Da questa domanda , sembra sensato che un controller crei un ViewModel che rifletta in modo più accurato il modello che la vista sta cercando di visualizzare, ma sono curioso di alcune convenzioni (sono nuovo del modello MVC , se non fosse già ovvio).

Fondamentalmente, ho avuto le seguenti domande:

  1. Normalmente mi piace avere una classe / file. Questo ha senso con un ViewModel se viene creato solo per trasferire dati da un controller a una vista?
  2. Se un ViewModel appartiene al suo stesso file e stai usando una struttura di directory / progetti per mantenere le cose separate, dove appartiene il file ViewModel ? Nella directory dei controller ?

Per ora è praticamente tutto. Potrei avere qualche altra domanda in arrivo, ma questo mi ha infastidito nell'ultima ora e mi sembra di trovare una guida coerente altrove.

EDIT: guardando l' app NerdDinner di esempio su CodePlex, sembra che i ViewModels facciano parte dei controller , ma mi fa ancora sentire a disagio che non siano nei propri file.


66
Non definirei esattamente NerdDinner un esempio di "Best Practices". Il tuo intuito ti serve bene. :)
Ryan Montgomery,

Risposte:


211

Creo quello che chiamo "ViewModel" per ogni vista. Li ho messi in una cartella chiamata ViewModels nel mio progetto Web MVC. Li chiamo dopo il controller e l'azione (o la vista) che rappresentano. Quindi, se devo passare i dati alla vista SignUp sul controller di appartenenza, creo una classe MembershipSignUpViewModel.cs e li inserisco nella cartella ViewModels.

Quindi aggiungo le proprietà e i metodi necessari per facilitare il trasferimento dei dati dal controller alla vista. Uso Automapper per passare dal mio ViewModel al modello di dominio e viceversa, se necessario.

Questo funziona bene anche con i ViewModel compositi che contengono proprietà del tipo di altri ViewModels. Ad esempio, se hai 5 widget nella pagina dell'indice nel controller di appartenenza e hai creato un ViewModel per ogni vista parziale - come passi i dati dall'azione Indice ai parziali? Si aggiunge una proprietà a MembershipIndexViewModel di tipo MyPartialViewModel e quando si esegue il rendering del parziale si passa in Model.MyPartialViewModel.

In questo modo è possibile regolare le proprietà ViewModel parziali senza modificare affatto la vista Indice. Passa ancora in Model.MyPartialViewModel, quindi c'è meno possibilità che tu debba passare attraverso l'intera catena di parziali per riparare qualcosa quando tutto ciò che stai facendo è aggiungere una proprietà al ViewModel parziale.

Aggiungerò anche lo spazio dei nomi "MyProject.Web.ViewModels" al web.config in modo da permettermi di fare riferimento a loro in qualsiasi vista senza mai aggiungere una dichiarazione di importazione esplicita su ogni vista. Lo rende solo un po 'più pulito.


3
Cosa succede se si desidera POST da una vista parziale e restituire l'intera vista (in caso di errore del modello)? Nella vista parziale non hai accesso al modello principale.
Cosmo,

5
@Cosmo: quindi POST di un'azione che può restituire l'intera vista in caso di errore del modello. Sul lato server, hai abbastanza per ricreare il modello genitore.
Tomas Aschan,

Che dire di un login [POST] e login [GET] azioni? con diversi modelli di visualizzazione?
Bart Calixto,

Di solito, il login [GET] non chiama ViewModel perché non è necessario caricare alcun dato.
Andre Figueiredo,

Ottimo consiglio Dove dovrebbero andare l'accesso ai dati, l'elaborazione e l'impostazione delle proprietà del modello / VM? Nel mio caso avremo alcuni dati provenienti da un database CMS locale e alcuni provenienti da servizi Web, che dovranno essere elaborati / manipolati prima di essere impostati su un modello. Mettere tutto ciò nel controller diventa piuttosto disordinato.
xr280xr

124

Separare le classi per categoria (Controller, ViewModels, Filtri ecc.) Non ha senso.

Se vuoi scrivere il codice per la sezione Home del tuo sito web (/), crea una cartella denominata Home e inserisci lì HomeController, IndexViewModel, AboutViewModel, ecc. E tutte le classi correlate utilizzate dalle azioni Home.

Se hai classi condivise, come un ApplicationController, puoi metterlo alla radice del tuo progetto.

Perché separare le cose correlate (HomeController, IndexViewModel) e tenere insieme le cose che non hanno alcuna relazione (HomeController, AccountController)?


Ho scritto un post sul blog su questo argomento.


13
Le cose diventeranno piuttosto confuse abbastanza rapidamente se lo fai.
UpTheCreek

14
No, disordinato è mettere tutti i controller in un dir / namespace. Se hai 5 controller, ognuno dei quali utilizza 5 viewmodel, allora hai 25 viewmodel. Namespace è il meccanismo per l'organizzazione del codice e non dovrebbe essere diverso qui.
Max Toro,

41
@Max Toro: sorpreso di essere stato sottovalutato così tanto. Dopo un po 'di tempo lavorando su ASP.Net MVC, sento molto dolore per avere tutti i ViewModels in un posto, tutti i controller in un altro e tutte le viste in un altro ancora. MVC è un trio di pezzi correlati, sono accoppiati - si supportano a vicenda. Sento che una soluzione può essere molto più organizzata se Controller, ViewModels e Views per una determinata sezione convivono nella stessa directory. MyApp / Accounts / Controller.cs, MyApp / Accounts / Create / ViewModel.cs, MyApp / Accounts / Create / View.cshtml, ecc.
quentin-starin

13
@RyanJMcGowan la separazione delle preoccupazioni non è la separazione delle classi.
Max Toro,

12
@RyanJMcGowan, indipendentemente dal modo in cui affronti lo sviluppo, il problema è quello con cui finisci, specialmente per le grandi app. Una volta in modalità di manutenzione non pensi a tutti i modelli, quindi a tutti i controller, aggiungi una funzione alla volta.
Max Toro,

21

Conservo le mie classi di applicazione in una sottocartella denominata "Core" (o una libreria di classi separata) e utilizzo gli stessi metodi del KIGG esempio ma con alcune lievi modifiche per rendere le mie applicazioni più ASCIUTTE.

Creo una classe BaseViewData in / Core / ViewData / dove memorizzo le proprietà comuni del sito.

Successivamente, creo anche tutte le classi ViewData della mia vista nella stessa cartella che derivano da BaseViewData e hanno proprietà specifiche della vista.

Quindi creo un ApplicationController da cui derivano tutti i miei controller. ApplicationController ha un metodo GetViewData generico come segue:

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

Infine, nella mia azione del controller faccio quanto segue per costruire il mio modello ViewData

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

Penso che funzioni davvero bene e mantenga le tue opinioni in ordine e i tuoi controller magri.


13

Una classe ViewModel è lì per incapsulare più pezzi di dati rappresentati da istanze di classi in un oggetto facile da gestire che puoi passare alla tua vista.

Avrebbe senso avere le classi ViewModel nei propri file, nella propria directory. Nei miei progetti ho una sottocartella della cartella Modelli chiamata ViewModels. Ecco dove vivono i miei ViewModels (ad es. ProductViewModel.cs).


13

Non esiste un buon posto in cui conservare i tuoi modelli. Puoi tenerli in un assemblaggio separato se il progetto è grande e ci sono molti ViewModels (Data Transfer Objects). Inoltre puoi tenerli in una cartella separata del progetto del sito. Ad esempio, in Oxite sono inseriti nel progetto Oxite che contiene anche molte classi diverse. I controller in Oxite vengono spostati in un progetto separato e anche le viste sono in un progetto separato.
In CodeCampServer i ViewModel sono denominati * Form e vengono inseriti nel progetto UI nella cartella Modelli.
In MvcPress progetto sono inseriti nel progetto Data, che contiene anche tutto il codice per lavorare con il database e un po 'di più (ma non ho raccomandato questo approccio, è solo per un esempio)
Quindi puoi vedere che ci sono molti punti di vista. Di solito mantengo i miei ViewModels (oggetti DTO) nel progetto del sito. Ma quando ho più di 10 modelli, preferisco spostarli in un gruppo separato. Di solito in questo caso sto spostando anche i controller per separare l'assemblaggio.
Un'altra domanda è come mappare facilmente tutti i dati dal modello al ViewModel. Suggerisco di dare un'occhiata alla libreria di AutoMapper . Mi piace moltissimo, fa tutto il lavoro sporco per me.
E suggerisco anche di guardare al progetto SharpArchitecture . Fornisce un'ottima architettura per i progetti e contiene molti framework e linee guida interessanti e un'ottima community.


8
ViewModels! = DTO
Bart Calixto,

6

ecco uno snippet di codice delle mie migliori pratiche:

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

5

Inseriamo tutti i nostri ViewModel nella cartella Modelli (tutta la nostra logica aziendale è in un progetto ServiceLayer separato)


4

Personalmente suggerirei se ViewModel è tutt'altro che banale, quindi utilizzare una classe separata.

Se hai più di un modello di visualizzazione, ti suggerisco che abbia senso partizionarlo in almeno una directory. se il modello di vista viene successivamente condiviso, lo spazio dei nomi implicito nella directory semplifica lo spostamento in un nuovo assembly.


2

Nel nostro caso abbiamo i modelli insieme ai controller in un progetto separato dalle viste.

Come regola generale, abbiamo cercato di spostare ed evitare la maggior parte delle cose ViewData ["..."] su ViewModel, quindi evitiamo casting e stringhe magiche, il che è una buona cosa.

ViewModel contiene anche alcune proprietà comuni come le informazioni di impaginazione per gli elenchi o le informazioni di intestazione della pagina per disegnare breadcrumb e titoli. In questo momento la classe base contiene troppe informazioni secondo me e potremmo dividerle in tre parti, le informazioni più basilari e necessarie per il 99% delle pagine su un modello di vista base, e quindi un modello per le liste e un modello per i moduli che contengono dati specifici per quegli scenari ed ereditano da quello di base.

Infine, implementiamo un modello di vista per ogni entità per gestire le informazioni specifiche.


0

codice nel controller:

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

codice in vista modello:

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       
    }

progetti:

  • DevJet.Web (il progetto Web ASP.NET MVC)

  • DevJet.Web.App.Dictionary (un progetto separato della libreria di classi)

    in questo progetto, ho creato alcune cartelle come: DAL, BLL, BO, VM (cartella per visualizzare i modelli)


Ciao, puoi condividere qual è la struttura della classe Entry?
Dinis Cruz,

0

Creare una classe base del modello di vista che ha proprietà comunemente richieste come risultato dell'operazione e dati contestuali, è inoltre possibile inserire dati e ruoli dell'utente corrente

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

Nella classe controller di base ha un metodo come PopulateViewModelBase () questo metodo riempirà i dati contestuali e i ruoli utente. HasError ed ErrorMessage, impostare queste proprietà in caso di eccezioni durante l'estrazione dei dati da service / db. Associare queste proprietà alla vista per mostrare l'errore. I ruoli utente possono essere utilizzati per mostrare la sezione nascondi nella vista in base ai ruoli.

Per popolare i modelli di vista in diverse azioni get, è possibile renderlo coerente disponendo di controller di base con metodo astratto FillModel

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

In controller

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;
}
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.