2009-09-23 7 views
12

Per coloro che creano ViewModels (per l'uso da viste digitate) in ASP.NET MVC, si preferisce recuperare i dati da un servizio/repository dal ViewModel o dalle classi controller?Recupero di dati all'interno di una classe ViewModel di ASP.NET MVC?

Per esempio, abbiamo iniziato avendo ViewModels essenzialmente essere DTOs e consentendo ai nostri controllori per recuperare i dati (ad esempio grossolanamente semplificato presuppone che l'utente può cambiare solo nome del dipendente):

public class EmployeeViewModel 
{ 
    public String Name; //posted back 
    public int Num; //posted back 
    public IEnumerable<Dependent> Dependents; //static 
    public IEnumerable<Spouse> Spouses; //static 
} 

public class EmployeeController() 
{ 
    ... 
    public ActionResult Employee(int empNum) 
    { 
     Models.EmployeeViewModel model = new Models.EmployeeViewModel(); 
     model.Name = _empSvc.FetchEmployee(empNum).Name; 
     model.Num = empNum; 
     model.Dependents = _peopleSvc.FetchDependentsForView(empNum); 
     model.Spouses = _peopleSvc.FetchDependentsForView(empNum); 
     return View(model); 
    } 

    [AcceptVerbs(HttpVerbs.Post)] 
    public ActionResult Employee(Models.EmployeeViewModel model) 
    { 
     if (!_empSvc.ValidateAndSaveName(model.Num, model.Name)) 
     { 
      model.Dependents = _peopleSvc.FetchDependentsForView(model.Num); 
      model.Spouses = _peopleSvc.FetchDependentsForView(model.Num); 
      return View(model); 
     } 
     this.RedirectToAction(c => c.Index()); 
    } 
} 
Questo

tutto sembrava bene fino a quando abbiamo iniziato a creare viste di grandi dimensioni (oltre 40 campi) con molti drop down e simili. Dato che le schermate avrebbero un'azione GET e POST (con il POST che restituisce una vista se c'era un errore di convalida), dovremmo duplicare il codice e rendere ViewModels più grande di quanto probabilmente dovrebbero essere.

Sto pensando che l'alternativa sarebbe quella di recuperare i dati tramite il servizio all'interno del ViewModel. La mia preoccupazione è che avremmo alcuni dati popolati dal ViewModel e alcuni dal controller (ad esempio nell'esempio sopra, il nome verrà popolato dal controller poiché è un valore pubblicato, mentre i dipendenti e i coniugi verranno popolati tramite tipo di funzione GetStaticData() nel ViewModel).

Pensieri?

+11

IEnumerable ? Cosa c'è nel modello poligamo? :-D –

risposta

7

Ho riscontrato lo stesso problema. Ho iniziato a creare classi per ogni azione quando il codice diventava troppo grande per i metodi di azione. Sì, si avrà il recupero dei dati in alcune classi e alcuni nei metodi del controller. L'alternativa è di avere tutti i dati recuperati nelle classi, ma metà delle classi di cui non avrete realmente bisogno, saranno create per coerenza o hanno tutti i dati recuperati nei metodi del controller, ma ancora una volta, alcuni di questi metodi saranno essere troppo complesso e necessario per essere stato astratto in classi ... quindi scegli il tuo veleno. Preferirei avere un po 'di incoerenza e avere la soluzione giusta per il lavoro.

Per quanto riguarda il comportamento nel ViewModel, io no, il punto di ViewModel deve essere una classe sottile per l'impostazione e l'estrazione di valori dalla vista.

Ci sono stati casi in cui ho messo i metodi di conversione nel ViewModel. Per esempio ho bisogno di convertire il ViewModel nell'entità corrispondente o ho bisogno di caricare ViewModel con i dati dall'entità.

Per rispondere alla tua domanda, preferisco recuperare i dati da con nel controller/metodi di azione.

In genere con DropDowns, creo un servizio di dropdown. I DropDown tendono ad essere gli stessi dati che attraversano le visualizzazioni. Con i menu a discesa in un servizio posso usarli su altre viste e/o memorizzarle nella cache.

A seconda del layout, oltre 40 campi potrebbero creare una vista disordinata. A seconda del tipo di dati, proverei a estendere molti campi su più viste con una sorta di interfaccia a schede o wizard.

+0

Grazie per il feedback. Questo è per un'applicazione client sicura, non un'app pubblica, quindi 40 campi di solito non sono ridicoli (sebbene la maggior parte delle nostre oltre 100 viste pianificate siano più vicine ai 10-15 campi). Quando dici di creare un servizio di dropdown, intendi nello stesso modo in cui ho fatto sopra, oppure il servizio popola un oggetto SelectList e lo passa al ViewModel? –

+0

Sembra che i tuoi dati dipendano dal dipendente attuale. Sembra a posto, purché funzioni per te. Quando mi riferivo a un servizio di dropdown, stavo pensando a un drop down di fusi orari o un drop down di paesi. Questo tipo di dati non cambia spesso e può essere facilmente memorizzato nella cache. –

+0

Abbiamo anche questo tipo di dati, che memorizziamo nella cache, ma chiamiamo allo stesso modo (la cache è gestita nel livello di servizio). Come gestiresti queste informazioni in cache in modo diverso, chiamandole nel tuo ViewModel? –

3

C'è di più ;-) È possibile recuperare nel raccoglitore modello o filtro azione. Per la seconda opzione, controlla il blog di Jimmy Bogard da qualche parte intorno allo here. Personalmente lo faccio in modellisti. Io uso ViewModel in questo modo: My custom ASP.NET MVC entity binding: is it a good solution?. Esso viene elaborato dal mio modello personalizzato legante:

public object BindModel(ControllerContext c, BindingContext b) 
{ 
    var id = b.ValueProvider[b.ModelName]; // don't remember exact syntax 
    var repository = ServiceLocator.GetInstance(GetRepositoryType(b.ModelType)); 
    var obj = repository.Get(id); 
    if (obj == null) 
    b.ModelState.AddModelError(b.ModelName, "Not found in database"); 
    return obj; 
} 

public ActionResult Action(EntityViewModel<Order> order) 
{ 
    if (!ModelState.IsValid) 
     ...; 
} 

È anche possibile vedere un esempio di modello di legante fare accesso al repository in S#arp Architecture.

Per quanto riguarda i dati statici nei modelli di visualizzazione, sto ancora esplorando gli approcci.Ad esempio, è possibile avere i vostri modelli vista ricordano le entità invece di liste, e

public class MyViewModel { MyViewModel pubblico (minimo d'ordine, IEmployeesSvc _svc) { }

public IList<Employee> GetEmployeesList() 
    { 
     return _svc.GetEmployeesFor(order.Number); 
    } 

}

Decidi come iniettare _svc in ViewModel, ma in pratica è lo stesso del controller. Fai attenzione che ViewModel viene creato anche da MVC tramite il costruttore senza parametri, quindi puoi utilizzare ServiceLocator o estendere MVC per la creazione di ViewModel, ad esempio all'interno del raccoglitore modello personalizzato. Oppure puoi utilizzare l'approccio di Jimmy Bogard con AutoMapper che supporta anche i contenitori IoC.

L'approccio comune qui è che ogni volta che vedo codice ripetitivo, cerco di eliminarlo. 100 azioni del controllore che eseguono il marshalling dominio-viewmodel più la ricerca di repository è un caso negativo. Un raccoglitore monomodello che lo fa in modo generico è buono.

1

Ecco un'altra soluzione: http://www.lostechies.com/blogs/jimmy_bogard/archive/2009/06/29/how-we-do-mvc-view-models.aspx

punti principali là:

  1. mappatura è eseguita da un mediatore - in questo caso è automapper ma può essere la propria classe (anche se più in codice). Ciò mantiene Dominio e ViewModel concentrati sulla logica di dominio/presentazione. Il mediatore (mapper) conterrà (per lo più automatico) la logica per la mappatura, inclusi i servizi iniettati.
  2. Il mapping viene applicato automaticamente, tutto ciò che si fa è dire al filtro azione i tipi di origine/destinazione: molto pulito.
  3. (Sembra essere importante per voi) AutoMapper supporta i mapping/tipi nidificati, quindi è possibile avere il ViewModel combinato con diversi modelli di visualizzazione indipendenti, in modo che il "DTO dello schermo" non sia disordinato.

Come in questo modello:

public class WholeViewModel 
{ 
    public Part1ViewModel ModelPart1 { get; set; } 
    public Part2ViewModel ModelPart2 { get; set; } 
} 

si riutilizzo mappature per le parti specifiche del tuo View, e non si scrive ogni nuova riga di codice, dal momento che ci stiamo già mappature per la modelli a vista parziale.

Se non si desidera automapper, si deve avere interfacce IViewModelMapper, e poi il contenitore CIO aiuterà il vostro filtro azione per trovare adeguate

container.Resolve(typeof(IViewModelMapper<>).MakeGenericType(mysourcetype, mydesttype)) 

e sarà anche fornire tutti i servizi esterni necessari a che mapper (questo è possibile anche con AutoMapper). Ma naturalmente AutoMapper può fare le ricorsioni e comunque, perché scrivere AutoMapper aggiuntivo ;-)

3

Non vorrei recuperare i dati dal database nel ViewModel. Il ViewModel esiste per promuovere la separazione delle preoccupazioni (tra la tua vista e il tuo modello). Aggrovigliare la logica della persistenza in questo tipo di sconfitte lo scopo.

Fortunatamente, il framework ASP.NET MVC ci offre più punti di integrazione, in particolare ModelBinder.

ho un'implementazione di un ModelBinder generica tirando le informazioni dal livello di servizio presso: -

http://www.iaingalloway.com/going-further-a-generic-servicelayer-modelbinder

Non usa un ViewModel, ma questo è facilmente risolto. Non è affatto l'unica implementazione. Per un progetto reale, probabilmente stai meglio con una soluzione meno generica e più personalizzata.

Se sei diligente, i tuoi metodi GET non hanno nemmeno bisogno di sapere che il livello di servizio esiste.

La soluzione probabilmente simile a: -

metodo di azione di controllo: -

public ActionResult Details(MyTypeIndexViewModel model) 
{ 
    if(ModelState.IsValid) 
    { 
    return View(model); 
    } 
    else 
    { 
    // Handle the case where the ModelState is invalid 
    // usually because they've requested MyType/Details/x 
    // and there's no matching MyType in the repository 
    // e.g. return RedirectToAction("Index") 
    } 
} 

ModelBinder: -

public object BindModel 
(
    ControllerContext controllerContext, 
    BindingContext bindingContext 
) 
{ 
    // Get the Primary Key from the requestValueProvider. 
    // e.g. bindingContext.ValueProvider["id"] 
    int id = ...; 

    // Get an instance of your service layer via your 
    // favourite dependancy injection framework. 
    // Or grab the controller's copy e.g. 
    // (controllerContext.Controller as MyController).Service 
    IMyTypeService service = ...; 

    MyType myType = service.GetMyTypeById(id) 

    if (myType == null) 
    { 
    // handle the case where the PK has no matching MyType in the repository 
    // e.g. bindingContext.ModelState.AddModelError(...) 
    } 


    MyTypeIndexViewModel model = new MyTypeIndexViewModel(myType); 

    // If you've got more repository calls to make 
    // (e.g. populating extra fields on the model) 
    // you can do that here. 

    return model; 
} 

ViewModel: -

public class MyTypeIndexViewModel 
{ 
    public MyTypeIndexViewModel(MyType source) 
    { 
    // Bind all the properties of the ViewModel in here, or better 
    // inherit from e.g. MyTypeViewModel, bind all the properties 
    // shared between views in there and chain up base(source) 
    } 
} 

Costruisci la tualivello di servizio e registrare ModelBinder normalmente.

+1

Le tue idee mi stanno intrigando e desidero iscrivermi alla tua newsletter. – anewcomer

0

Considerare di passare i propri servizi nel ViewModel personalizzato sul suo costruttore (ala Iniezione di dipendenza). Ciò rimuove il codice della popolazione del modello dal controller e consente di concentrarsi sul controllo del flusso logico dell'applicazione. Custom ViewModels sono il luogo ideale per astrarre la preparazione di cose come SelectList da cui dipenderanno i tuoi droplists.

Un sacco di codice nel controller per cose come il recupero dei dati non è considerato una buona pratica. La responsabilità principale del controllore è di "controllare" il flusso dell'applicazione.

0

Inoltrare questo in ritardo ... La taglia è quasi finita. Ma ...

Un'altra mapper da guardare è Automapper: http://www.codeplex.com/AutoMapper

E panoramica su come usarlo: http://www.lostechies.com/blogs/jimmy_bogard/archive/2009/01/22/automapper-the-object-object-mapper.aspx

mi piace molto è la sintassi.

// place this somewhere in your globals, or base controller constructor 
Mapper.CreateMap<Employee, EmployeeViewModel>(); 

Ora, nel controller, vorrei utilizzare più viewmodels. Ciò impone l'utilizzo di DRY consentendo di riutilizzare i modelli di visualizzazione altrove nella propria applicazione. Non li legherei tutti a 1 viewmodel. Vorrei refactoring a qualcosa di simile:

public class EmployeeController() 
{ 
    private IEmployeeService _empSvc; 
    private ISpouseService _peopleSvc; 

    public EmployeeController(
     IEmployeeService empSvc, ISpouseService peopleSvc) 
    { 
    // D.I. hard at work! Auto-wiring up our services. :) 
    _empSvc = empSvc; 
    _peopleSvc = peopleSvc; 

    // setup all ViewModels here that the controller would use 
    Mapper.CreateMap<Employee, EmployeeViewModel>(); 
    Mapper.CreateMap<Spouse, SpouseViewModel>(); 
    } 

    public ActionResult Employee(int empNum) 
    { 
    // really should have some validation here that reaches into the domain 
    // 

    var employeeViewModel = 
     Mapper.Map<Employee, EmployeeViewModel>(
      _empSvc.FetchEmployee(empNum) 
     ); 

    var spouseViewModel = 
     Mapper.Map<Spouses, SpousesViewModel>(
      _peopleSvc.FetchSpouseByEmployeeID(empNum) 
     ); 

    employeeViewModel.SpouseViewModel = spouseViewModel; 

    return View(employeeViewModel);  
    } 

    [AcceptVerbs(HttpVerbs.Post)] 
    public ActionResult Employee(int id, FormCollection values)  
    { 
    try 
    { 
     // always post to an ID, which is the employeeID 
     var employee = _empSvc.FetchEmployee(id); 

     // and bind using the built-in UpdateModel helpers. 
     // this will throw an exception if someone is posting something 
     // they shouldn't be posting. :) 
     UpdateModel(employee); 

     // save employee here 

     this.RedirectToAction(c => c.Index()); 
    } 
    catch 
    { 
     // check your domain model for any errors. 
     // check for any other type of exception. 
     // fail back to the employee screen 
     RedirectToAction(c => c.Employee(id)); 
    } 
    } 
} 

In genere cerco di stare lontano dal salvare più entità su un'azione del controller. Invece, vorrei refactoring l'oggetto dominio dipendente di avere metodi AddSpouse() e SaveSpouse(), che avrebbe preso un oggetto di Spouse. Questo concetto è noto come AggregateRoots, che controlla tutte le dipendenze dalla radice, che è l'oggetto Employee(). Ma quello sono solo io.

Problemi correlati