Usare ViewData e implementare classi ViewModel

di Microsoft

Scarica il PDF

Questo è il passaggio 6 di un'esercitazione gratuita sull'applicazione "NerdDinner" che illustra come creare un'applicazione Web di piccole dimensioni, ma completa, usando ASP.NET MVC 1.

Il passaggio 6 illustra come abilitare il supporto per scenari di modifica dei moduli più avanzati e illustra anche due approcci che possono essere usati per passare i dati dai controller alle visualizzazioni: ViewData e ViewModel.

Se si usa ASP.NET MVC 3, è consigliabile seguire le esercitazioni Introduzione Con MVC 3 o MVC Music Store.

NerdDinner Passaggio 6: ViewData e ViewModel

Sono stati illustrati diversi scenari post di modulo e si è discusso di come implementare il supporto per la creazione, l'aggiornamento e l'eliminazione (CRUD). L'implementazione di DinnersController verrà ora ulteriormente illustrata e verrà abilitato il supporto per scenari di modifica dei moduli più avanzati. In questo modo verranno illustrati due approcci che possono essere usati per passare i dati dai controller alle visualizzazioni: ViewData e ViewModel.

Passaggio dei dati dai titolari del trattamento ai View-Templates

Una delle caratteristiche di definizione del modello MVC è la rigorosa "separazione delle preoccupazioni" che consente di applicare tra i diversi componenti di un'applicazione. I modelli, i controller e le visualizzazioni hanno ruoli e responsabilità ben definiti e comunicano tra loro in modi ben definiti. Ciò consente di promuovere la verificabilità e il riutilizzo del codice.

Quando una classe Controller decide di eseguire il rendering di una risposta HTML a un client, è responsabile del passaggio esplicito al modello di visualizzazione di tutti i dati necessari per il rendering della risposta. I modelli di visualizzazione non devono mai eseguire alcun recupero dati o logica dell'applicazione e devono invece limitarsi a disporre solo di codice di rendering basato sul modello o sui dati passati dal controller.

Al momento i dati del modello passati dalla classe DinnersController ai modelli di visualizzazione sono semplici e semplici, ovvero un elenco di oggetti Dinner nel caso di Index() e un singolo oggetto Dinner nel caso di Details(), Edit(), Create() e Delete(). Man mano che si aggiungono altre funzionalità dell'interfaccia utente all'applicazione, è spesso necessario passare più di questi dati per eseguire il rendering delle risposte HTML all'interno dei modelli di visualizzazione. Ad esempio, potrebbe essere necessario modificare il campo "Paese" all'interno della casella di testo Modifica e Crea visualizzazioni in una casella di testo HTML in un elenco a discesa. Anziché impostare come hardcoded l'elenco a discesa dei nomi di paese e area geografica nel modello di visualizzazione, è consigliabile generarlo da un elenco di paesi e aree supportati popolati in modo dinamico. Sarà necessario un modo per passare sia l'oggetto Dinner che l'elenco di paesi e aree supportati dal controller ai modelli di visualizzazione.

Verranno ora esaminati due modi per eseguire questa operazione.

Uso del dizionario ViewData

La classe di base Controller espone una proprietà del dizionario "ViewData" che può essere utilizzata per passare elementi di dati aggiuntivi dai controller alle viste.

Ad esempio, per supportare lo scenario in cui si vuole modificare la casella di testo "Country" all'interno della visualizzazione Modifica da una casella di testo HTML a un elenco a discesa, è possibile aggiornare il metodo di azione Edit() per passare (oltre a un oggetto Dinner) un oggetto SelectList che può essere usato come modello di un elenco a discesa "Paesi".

//
// GET: /Dinners/Edit/5

[Authorize]
public ActionResult Edit(int id) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    ViewData["Countries"] = new SelectList(PhoneValidator.AllCountries, dinner.Country);

    return View(dinner);
}

Il costruttore di SelectList precedente accetta un elenco di paesi e aree geografiche con cui popolare l'elenco a discesa, nonché il valore attualmente selezionato.

È quindi possibile aggiornare il modello di visualizzazione Edit.aspx per usare il metodo helper Html.DropDownList() anziché il metodo helper Html.TextBox() usato in precedenza:

<%= Html.DropDownList("Country", ViewData["Countries"] as SelectList) %>

Il metodo helper Html.DropDownList() precedente accetta due parametri. Il primo è il nome dell'elemento del modulo HTML da restituire. Il secondo è il modello "SelectList" passato tramite il dizionario ViewData. Viene usata la parola chiave "as" C# per eseguire il cast del tipo all'interno del dizionario come SelectList.

E ora quando si esegue l'applicazione e si accede all'URL /Dinners/Edit/1 all'interno del browser, si noterà che l'interfaccia utente di modifica è stata aggiornata per visualizzare un elenco a discesa di paesi e aree geografiche anziché una casella di testo:

Screenshot dell'interfaccia utente di modifica con l'elenco a discesa di paesi e aree geografiche evidenziati con una freccia rossa.

Poiché viene eseguito anche il rendering del modello di visualizzazione Edit dal metodo HTTP-POST Edit (in scenari in cui si verificano errori), è necessario assicurarsi di aggiornare anche questo metodo per aggiungere SelectList a ViewData quando viene eseguito il rendering del modello di visualizzazione in scenari di errore:

//
// POST: /Dinners/Edit/5

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection collection) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    try {
    
        UpdateModel(dinner);

        dinnerRepository.Save();

        return RedirectToAction("Details", new { id=dinner.DinnerID });
    }
    catch {
    
        ModelState.AddModelErrors(dinner.GetRuleViolations());

        ViewData["countries"] = new SelectList(PhoneValidator.AllCountries, dinner.Country);

        return View(dinner);
    }
}

Ora lo scenario di modifica DinnersController supporta un oggetto DropDownList.

Uso di un modello ViewModel

L'approccio del dizionario ViewData offre il vantaggio di essere abbastanza veloce e facile da implementare. Alcuni sviluppatori non amano usare dizionari basati su stringhe, tuttavia, poiché gli errori di digitazione possono causare errori che non verranno rilevati in fase di compilazione. Il dizionario ViewData non tipizzato richiede anche l'uso dell'operatore "as" o il cast quando si usa un linguaggio fortemente tipizzato come C# in un modello di visualizzazione.

Un approccio alternativo che è possibile usare è spesso definito modello "ViewModel". Quando si usa questo modello vengono create classi fortemente tipizzate ottimizzate per gli scenari di visualizzazione specifici e che espongono proprietà per i valori/contenuti dinamici necessari per i modelli di visualizzazione. Le classi controller possono quindi popolare e passare queste classi ottimizzate per la visualizzazione al modello di visualizzazione da usare. In questo modo, il controllo in fase di compilazione e l'intellisense dell'editor vengono abilitati nei modelli di visualizzazione.

Ad esempio, per abilitare gli scenari di modifica dei moduli di cena, è possibile creare una classe "DinnerFormViewModel" come illustrato di seguito che espone due proprietà fortemente tipizzate: un oggetto Dinner e il modello SelectList necessario per popolare l'elenco a discesa "Paesi":

public class DinnerFormViewModel {

    // Properties
    public Dinner     Dinner    { get; private set; }
    public SelectList Countries { get; private set; }

    // Constructor
    public DinnerFormViewModel(Dinner dinner) {
        Dinner = dinner;
        Countries = new SelectList(PhoneValidator.AllCountries, dinner.Country);
    }
}

È quindi possibile aggiornare il metodo di azione Edit() per creare DinnerFormViewModel usando l'oggetto Dinner recuperato dal repository e quindi passarlo al modello di visualizzazione:

//
// GET: /Dinners/Edit/5

[Authorize]
public ActionResult Edit(int id) {

    Dinner dinner = dinnerRepository.GetDinner(id);
    
    return View(new DinnerFormViewModel(dinner));
}

Il modello di visualizzazione verrà quindi aggiornato in modo che sia previsto un oggetto "DinnerFormViewModel" anziché un oggetto "Dinner" modificando l'attributo "inherits" nella parte superiore della pagina edit.aspx come indicato di seguito:

Inherits="System.Web.Mvc.ViewPage<NerdDinner.Controllers.DinnerFormViewModel>

Una volta eseguita questa operazione, l'intellisense della proprietà "Model" all'interno del modello di visualizzazione verrà aggiornato in modo da riflettere il modello a oggetti del tipo DinnerFormViewModel che verrà passato:

Screenshot della finestra dell'editor di codice con un elenco a discesa e l'elemento Elenco cena evidenziato con un rettangolo blu.

Screenshot della finestra dell'editor di codice con un elenco a discesa e l'elemento elenco indirizzi evidenziato con un rettangolo punteggiato grigio.

È quindi possibile aggiornare il codice di visualizzazione per disattivarlo. Si noti che di seguito non vengono modificati i nomi degli elementi di input creati (gli elementi del modulo verranno comunque denominati "Title", "Country"), ma si aggiornano i metodi helper HTML per recuperare i valori usando la classe DinnerFormViewModel:

<p>
    <label for="Title">Dinner Title:</label>
    <%= Html.TextBox("Title", Model.Dinner.Title) %>
    <%=Html.ValidationMessage("Title", "*") %>
</p>

<p>
    <label for="Country">Country:</label>
    <%= Html.DropDownList("Country", Model.Countries) %>                
    <%=Html.ValidationMessage("Country", "*") %>
</p>

Si aggiornerà anche il metodo Edit post per usare la classe DinnerFormViewModel durante il rendering degli errori:

//
// POST: /Dinners/Edit/5

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection collection) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    try {
        UpdateModel(dinner);

        dinnerRepository.Save();

        return RedirectToAction("Details", new { id=dinner.DinnerID });
    }
    catch {
        ModelState.AddModelErrors(dinner.GetRuleViolations());

        return View(new DinnerFormViewModel(dinner));
    }
}

È anche possibile aggiornare i metodi di azione Create() per riutilizzare la stessa classe DinnerFormViewModel per abilitare anche l'elenco a discesa "Paesi" all'interno di tali metodi. Di seguito è riportata l'implementazione HTTP-GET:

//
// GET: /Dinners/Create

public ActionResult Create() {

    Dinner dinner = new Dinner() {
        EventDate = DateTime.Now.AddDays(7)
    };

    return View(new DinnerFormViewModel(dinner));
}

Di seguito è riportata l'implementazione del metodo HTTP-POST Create:

//
// POST: /Dinners/Create

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(Dinner dinner) {

    if (ModelState.IsValid) {

        try {
            dinner.HostedBy = "SomeUser";

            dinnerRepository.Add(dinner);
            dinnerRepository.Save();

            return RedirectToAction("Details", new { id=dinner.DinnerID });
        }
        catch {
            ModelState.AddModelErrors(dinner.GetRuleViolations());
        }
    }

    return View(new DinnerFormViewModel(dinner));
}

Ora sia gli elenchi a discesa Modifica che Crea supportano gli elenchi a discesa per la selezione del paese o dell'area geografica.

Classi ViewModel a forma personalizzata

Nello scenario precedente, la classe DinnerFormViewModel espone direttamente l'oggetto modello Dinner come proprietà, insieme a una proprietà del modello SelectList di supporto. Questo approccio funziona correttamente per gli scenari in cui l'interfaccia utente HTML che si vuole creare all'interno del modello di visualizzazione corrisponde relativamente strettamente agli oggetti del modello di dominio.

Per gli scenari in cui questo non è il caso, un'opzione che è possibile usare consiste nel creare una classe ViewModel a forma personalizzata il cui modello a oggetti è più ottimizzato per l'utilizzo dalla visualizzazione e che potrebbe essere completamente diverso dall'oggetto modello di dominio sottostante. Ad esempio, potrebbe esporre nomi di proprietà diversi e/o proprietà di aggregazione raccolte da più oggetti modello.

Le classi ViewModel con forma personalizzata possono essere usate sia per passare i dati dai controller alle visualizzazioni di cui eseguire il rendering, sia per gestire i dati del modulo inviati al metodo di azione di un controller. Per questo scenario successivo, è possibile che il metodo di azione aggiorni un oggetto ViewModel con i dati inseriti nel modulo e quindi usi l'istanza viewModel per eseguire il mapping o recuperare un oggetto modello di dominio effettivo.

Le classi ViewModel con forma personalizzata possono offrire una notevole flessibilità e sono un elemento da analizzare ogni volta che si trova il codice di rendering all'interno dei modelli di visualizzazione o il codice di pubblicazione dei moduli all'interno dei metodi di azione che iniziano a diventare troppo complicati. Questo è spesso un segno che i modelli di dominio non corrispondono correttamente all'interfaccia utente generata e che una classe ViewModel a forma intermedia può essere utile.

passaggio successivo

Si esamini ora come è possibile usare parziali e pagine master per riutilizzare e condividere l'interfaccia utente nell'applicazione.