Come mappare il modello di visualizzazione di nuovo al modello di dominio in un'azione POST?


87

Ogni articolo trovato in Internet sull'uso di ViewModels e sull'utilizzo di Automapper fornisce le linee guida della mappatura "Controller -> View". Prendi un modello di dominio insieme a tutti gli elenchi di selezione in un ViewModel specializzato e lo passi alla vista. È chiaro e va bene.
La vista ha un modulo e alla fine siamo nell'azione POST. Qui tutti i Model Binders entrano in scena insieme a [ovviamente] un altro View Model che è [ovviamente] correlato al ViewModel originale almeno nella parte delle convenzioni di denominazione per motivi di binding e validazione.

Come lo associ al tuo modello di dominio?

Lascia che sia un'azione di inserimento, potremmo usare lo stesso Automapper. Ma cosa succede se si tratta di un'azione di aggiornamento? Dobbiamo recuperare la nostra entità di dominio dal repository, aggiornare le sue proprietà in base ai valori nel ViewModel e salvare nel repository.

APPENDICE 1 (9 febbraio 2010): A volte, l'assegnazione delle proprietà del modello non è sufficiente. Dovrebbe essere intrapresa un'azione contro il modello di dominio in base ai valori di Visualizza modello. Vale a dire, alcuni metodi dovrebbero essere chiamati su Domain Model. Probabilmente, dovrebbe esserci una sorta di livello di servizio dell'applicazione che si trova tra il controller e il dominio per elaborare i modelli di visualizzazione ...


Come organizzare questo codice e dove posizionarlo per raggiungere i seguenti obiettivi?

  • mantenere i controller sottili
  • onorare la pratica SoC
  • seguire i principi di Domain Driven Design
  • essere ASCIUTTO
  • continua ...

Risposte:


37

Uso un'interfaccia IBuilder e la implemento utilizzando ValueInjecter

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

... (implementazione) RebuildViewModel chiama soloBuildViewModel(BuilEntity(viewModel))

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

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

btw non scrivo ViewModel scrivo Input perché è molto più breve, ma non è molto importante
spero che aiuti

Aggiornamento: sto usando questo approccio ora nell'app ProDinner ASP.net MVC Demo , ora si chiama IMapper, c'è anche un pdf fornito dove questo approccio è spiegato in dettaglio


Mi piace questo approccio. Una cosa che non mi è chiara è l'implementazione di IBuilder, soprattutto alla luce di un'applicazione a più livelli. Ad esempio, il mio ViewModel ha 3 SelectLists. In che modo l'implementazione del builder recupera i valori dell'elenco di selezione dal repository?
Matt Murrell

@ Matt Murrell guarda prodinner.codeplex.com Lo faccio lì, e lo chiamo IMapper invece di IBuilder
Omu

6
Mi piace questo approccio, ne ho implementato un esempio qui: gist.github.com/2379583
Paul Stovell

A mio avviso non è conforme all'approccio del modello di dominio. Sembra un approccio CRUD per requisiti poco chiari. Non dovremmo usare Factories (DDD) e metodi correlati nel Domain Model per trasmettere alcune azioni ragionevoli? In questo modo faremmo meglio a caricare un'entità dal DB e aggiornarla come richiesto, giusto? Quindi sembra che non sia completamente corretto.
Artyom

7

Strumenti come AutoMapper possono essere utilizzati per aggiornare l'oggetto esistente con i dati dall'oggetto di origine. L'azione del controller per l'aggiornamento potrebbe essere simile a:

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

A parte ciò che è visibile nello snippet sopra:

  • I dati POST per visualizzare il modello + la convalida vengono eseguiti in ModelBinder (potrebbero essere estesi con associazioni personalizzate)
  • La gestione degli errori (cioè la cattura di eccezioni di accesso ai dati da parte del repository) può essere eseguita dal filtro [HandleError]

L'azione del controller è piuttosto ridotta e le preoccupazioni sono separate: i problemi di mappatura vengono risolti nella configurazione di AutoMapper, la convalida viene eseguita da ModelBinder e l'accesso ai dati da Repository.


6
Non sono sicuro che Automapper sia utile qui poiché non può invertire l'appiattimento. Dopotutto, Domain Model non è un semplice DTO come View Model, quindi potrebbe non essere sufficiente assegnargli alcune proprietà. Probabilmente, alcune azioni dovrebbero essere eseguite contro il modello di dominio in base ai contenuti di Visualizza modello. Tuttavia, +1 per la condivisione di un approccio abbastanza buono.
Anthony Serdyukov

@Anton ValueInjecter può invertire l'appiattimento;)
Omu

con questo approccio non si mantiene il controller sottile, si viola SoC e DRY ... come ha detto Omu, si dovrebbe avere uno strato separato che si occupi della mappatura.
Rookian

5

Vorrei dire che riutilizzi il termine ViewModel per entrambe le direzioni dell'interazione con il cliente. Se hai letto abbastanza codice ASP.NET MVC in natura, probabilmente avrai notato la distinzione tra ViewModel e EditModel. Penso che sia importante.

Un ViewModel rappresenta tutte le informazioni richieste per eseguire il rendering di una vista. Ciò potrebbe includere dati resi in luoghi statici non interattivi e anche dati puramente per eseguire un controllo per decidere cosa esattamente rendere. Un'azione GET del controller è generalmente responsabile della creazione del pacchetto di ViewModel per la sua visualizzazione.

Un EditModel (o forse un ActionModel) rappresenta i dati richiesti per eseguire l'azione che l'utente desiderava eseguire per quel POST. Quindi un EditModel sta davvero cercando di descrivere un'azione. Questo probabilmente escluderà alcuni dati dal ViewModel e sebbene siano correlati penso sia importante rendersi conto che sono effettivamente diversi.

Un'idea

Detto questo, potresti facilmente avere una configurazione AutoMapper per andare da Model -> ViewModel e un'altra per andare da EditModel -> Model. Quindi le diverse azioni del controller devono solo utilizzare AutoMapper. Hell the EditModel potrebbe avere una funzione su di esso per convalidare le sue proprietà rispetto al modello e per applicare quei valori al modello stesso. Non sta facendo nient'altro e hai ModelBinders in MVC per mappare comunque la richiesta a EditModel.

Un'altra idea

Oltre a ciò a cui ho pensato di recente, questo tipo di funzionamento dell'idea di un ActionModel è che ciò che il client ti sta inviando è in realtà la descrizione di diverse azioni eseguite dall'utente e non solo un grande gruppo di dati. Ciò richiederebbe certamente un po 'di Javascript sul lato client per essere gestito, ma penso che l'idea sia intrigante.

Essenzialmente quando l'utente esegue le azioni sullo schermo che hai presentato, Javascript inizierà a creare un elenco di oggetti azione. Un esempio è possibile che l'utente si trovi in ​​una schermata di informazioni sui dipendenti. Aggiornano il cognome e aggiungono un nuovo indirizzo perché il dipendente è stato recentemente sposato. Sotto le coperte questo produce un ChangeEmployeeNamee un AddEmployeeMailingAddressoggetti a una lista. L'utente fa clic su "Salva" per confermare le modifiche e si invia l'elenco di due oggetti, ciascuno contenente solo le informazioni necessarie per eseguire ciascuna azione.

Avresti bisogno di un ModelBinder più intelligente di quello predefinito ma un buon serializzatore JSON dovrebbe essere in grado di occuparsi della mappatura degli oggetti azione lato client su quelli lato server. Quelli lato server (se ti trovi in ​​un ambiente a 2 livelli) potrebbero facilmente avere metodi che completano l'azione sul Modello con cui lavorano. Quindi l'azione Controller finisce per ottenere solo un ID per l'istanza del modello da estrarre e un elenco di azioni da eseguire su di essa. Oppure le azioni contengono l'id per tenerle molto separate.

Quindi forse qualcosa del genere viene realizzato sul lato server:

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
}

Ciò rende davvero l'azione di post-back abbastanza generica poiché ti affidi al tuo ModelBinder per ottenere l'istanza IUserAction corretta e l'istanza IUserAction per eseguire la logica corretta o (più probabilmente) chiamare il modello con le informazioni.

Se ti trovi in ​​un ambiente a 3 livelli, IUserAction potrebbe essere semplicemente reso semplice DTO da sparare attraverso il confine ed eseguito in un metodo simile sul livello dell'app. A seconda di come si esegue quel livello, potrebbe essere suddiviso molto facilmente e rimanere comunque in una transazione (ciò che viene in mente è la richiesta / risposta di Agatha e sfruttare la mappa dell'identità di DI e NHibernate).

Ad ogni modo sono sicuro che non sia un'idea perfetta, richiederebbe un po 'di JS lato client per essere gestito, e non sono ancora stato in grado di fare un progetto per vedere come si svolge, ma il post stava cercando di pensare a come arrivare e tornare di nuovo così ho pensato di dare i miei pensieri. Spero che aiuti e mi piacerebbe conoscere altri modi per gestire le interazioni.


Interessante. Per quanto riguarda la distinzione tra ViewModel e EditModel ... stai suggerendo che per una funzione di modifica utilizzeresti un ViewModel per creare il modulo e poi ti legheresti a un EditModel quando l'utente lo ha pubblicato? In tal caso, come gestireste le situazioni in cui dovreste ripubblicare il modulo a causa di errori di convalida (ad esempio quando ViewModel conteneva elementi per popolare un menu a discesa) - includereste anche gli elementi a discesa in EditModel? In tal caso, quale sarebbe la differenza tra i due?
UpTheCreek

Immagino che la tua preoccupazione sia che se uso un EditModel e si verifica un errore, devo ricostruire il mio ViewModel che potrebbe essere molto costoso. Direi semplicemente ricostruire il ViewModel e assicurarsi che abbia un posto dove mettere i messaggi di notifica dell'utente (probabilmente sia positivi che negativi come gli errori di convalida). Se risulta essere un problema di prestazioni, puoi sempre memorizzare nella cache il ViewModel fino al termine della richiesta successiva di quella sessione (probabilmente il post di EditModel).
Sean Copenhaver

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.