Fornire il supporto per operazioni di creazione, lettura, aggiornamento ed eliminazione sulle voci di moduli di dati

da Microsoft

Scarica il PDF

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

Il passaggio 5 illustra come prendere ulteriormente la classe DinnersController abilitando il supporto per la modifica, la creazione ed l'eliminazione di Cena con esso.

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

NerdDinner Passaggio 5: Creare, aggiornare, eliminare scenari di modulo

Sono stati introdotti controller e visualizzazioni e sono stati illustrati come usarli per implementare un'esperienza di presentazione/dettagli per le cene sul sito. Il nostro passaggio successivo sarà quello di portare ulteriormente la nostra classe DinnersController e abilitare il supporto per la modifica, la creazione e l'eliminazione di cena con esso.

URL gestiti da DinnersController

In precedenza sono stati aggiunti metodi di azione a DinnersController che hanno implementato il supporto per due URL: /Dinners e /Dinners/Details/[id].

URL VERBO Scopo
/Cene/ GET Visualizzare un elenco HTML delle prossime cene.
/Dinners/Details/[id] GET Visualizza i dettagli relativi a una cena specifica.

Verranno ora aggiunti metodi di azione per implementare tre URL aggiuntivi: /Dinners/Edit/[id], /Dinners/Create e /Dinners/Delete/[id]. Questi URL consentiranno il supporto per la modifica delle cene esistenti, la creazione di nuove cene e l'eliminazione di cena.

Verranno supportate sia le interazioni verbo HTTP GET che HTTP POST con questi nuovi URL. Le richieste HTTP GET a questi URL visualizzeranno la visualizzazione HTML iniziale dei dati (un modulo popolato con i dati Dinner nel caso di "modifica", un modulo vuoto nel caso di "create" e una schermata di conferma di eliminazione nel caso di "eliminazione"). Le richieste HTTP POST a questi URL salvano/aggiornano/eliminano i dati della cena nella cenaRepository (e da qui al database).

URL VERBO Scopo
/Dinners/Edit/[id] GET Visualizzare un modulo HTML modificabile popolato con i dati Di cena.
POST Salvare le modifiche del modulo per una determinata cena nel database.
/Dinners/Create GET Visualizzare un modulo HTML vuoto che consente agli utenti di definire nuove cene.
POST Creare una nuova cena e salvarla nel database.
/Dinners/Delete/[id] GET Visualizza la schermata di conferma dell'eliminazione.
POST Elimina la cena specificata dal database.

Modifica supporto

Iniziamo implementando lo scenario "modifica".

Metodo di azione di modifica HTTP-GET

Si inizierà implementando il comportamento HTTP "GET" del metodo di azione di modifica. Questo metodo verrà richiamato quando viene richiesto l'URL /Dinners/Edit/[id]. L'implementazione avrà un aspetto simile al seguente:

//
// GET: /Dinners/Edit/2

public ActionResult Edit(int id) {

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

Il codice precedente usa DinnerRepository per recuperare un oggetto Dinner. Esegue quindi il rendering di un modello View usando l'oggetto Dinner. Poiché non è stato passato in modo esplicito un nome modello al metodo helper View(), verrà usato il percorso predefinito basato sulla convenzione per risolvere il modello di visualizzazione: /Views/Dinners/Edit.aspx.

Verrà ora creato questo modello di visualizzazione. Questa operazione verrà eseguita facendo clic con il pulsante destro del mouse all'interno del metodo Edit e selezionando il comando di menu di scelta rapida "Aggiungi visualizzazione":

Screenshot della creazione di un modello di visualizzazione per aggiungere la visualizzazione in Visual Studio.

All'interno della finestra di dialogo "Aggiungi visualizzazione" verrà indicato che si passa un oggetto Dinner al modello di visualizzazione come modello e si sceglie di eseguire automaticamente lo scaffolding di un modello "Modifica":

Screenshot di Aggiungi visualizzazione a scaffolding automatico di un modello di modifica.

Quando si fa clic sul pulsante "Aggiungi", Visual Studio aggiungerà un nuovo file di modello di visualizzazione "Edit.aspx" all'interno della directory "\Views\Dinners". Aprirà anche il nuovo modello di visualizzazione "Edit.aspx" all'interno dell'editor di codice, popolato con un'implementazione di scaffold iniziale "Modifica" come illustrato di seguito:

Screenshot del nuovo modello di visualizzazione Modifica all'interno dell'editor di codice.

Si apportano alcune modifiche all'scaffold predefinito "edit" generato e si aggiorna il modello di visualizzazione di modifica per avere il contenuto seguente (che rimuove alcune delle proprietà che non si desidera esporre):

<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
    Edit: <%=Html.Encode(Model.Title)%>
</asp:Content>

<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Edit Dinner</h2>

    <%=Html.ValidationSummary("Please correct the errors and try again.") %>  
    
    <% using (Html.BeginForm()) { %>

        <fieldset>
            <p>
                <label for="Title">Dinner Title:</label>
                <%=Html.TextBox("Title") %>
                <%=Html.ValidationMessage("Title", "*") %>
            </p>
            <p>
                <label for="EventDate">EventDate:</label>
                <%=Html.TextBox("EventDate", String.Format("{0:g}", Model.EventDate))%>
                <%=Html.ValidationMessage("EventDate", "*") %>
            </p>
            <p>
                <label for="Description">Description:</label>
                <%=Html.TextArea("Description") %>
                <%=Html.ValidationMessage("Description", "*")%>
            </p>
            <p>
                <label for="Address">Address:</label>
                <%=Html.TextBox("Address") %>
                <%=Html.ValidationMessage("Address", "*") %>
            </p>
            <p>
                <label for="Country">Country:</label>
                <%=Html.TextBox("Country") %>               
                <%=Html.ValidationMessage("Country", "*") %>
            </p>
            <p>
                <label for="ContactPhone">ContactPhone #:</label>
                <%=Html.TextBox("ContactPhone") %>
                <%=Html.ValidationMessage("ContactPhone", "*") %>
            </p>
            <p>
                <label for="Latitude">Latitude:</label>
                <%=Html.TextBox("Latitude") %>
                <%=Html.ValidationMessage("Latitude", "*") %>
            </p>
            <p>
                <label for="Longitude">Longitude:</label>
                <%=Html.TextBox("Longitude") %>
                <%=Html.ValidationMessage("Longitude", "*") %>
            </p>
            <p>
                <input type="submit" value="Save"/>
            </p>
        </fieldset>
        
    <% } %>
    
</asp:Content>

Quando si esegue l'applicazione e si richiede l'URL "/Dinners/Edit/1" verrà visualizzata la pagina seguente:

Screenshot della pagina Applicazione M V C.

Il markup HTML generato dalla visualizzazione è simile al seguente. È HTML standard: con un elemento modulo> che esegue un <POST HTTP all'URL /Dinners/Edit/1 quando viene premuto il pulsante "Salva" <tipo di input="submit".> Un tipo di input HTML <="text"/> è stato restituito per ogni proprietà modificabile:

Screenshot del markup H T M L generato.

Metodi helper Html.BeginForm() e Html.TextBox()

Il modello di visualizzazione "Edit.aspx" usa diversi metodi "Helper Html": Html.ValidationSummary(), Html.BeginForm(), Html.TextBox() e Html.ValidationMessage(). Oltre alla generazione di markup HTML per noi, questi metodi helper forniscono la gestione e il supporto di convalida degli errori predefiniti.

Metodo helper Html.BeginForm()

Il metodo helper Html.BeginForm() è l'output dell'elemento modulo> HTML <nel markup. Nel modello di visualizzazione Edit.aspx si noterà che si sta applicando un'istruzione C# "using" quando si usa questo metodo. La parentesi graffe aperta indica l'inizio del contenuto del <modulo> e la parentesi graffe di chiusura è ciò che indica la fine dell'elemento </form> :

<% using (Html.BeginForm()) { %>

   <fieldset>
   
      <!-- Fields Omitted for Brevity -->
   
      <p>
         <input type="submit" value="Save"/>
      </p>
   </fieldset>
   
<% } %>

In alternativa, se si trova l'approccio di istruzione "using" innaturale per uno scenario simile a questo, è possibile usare una combinazione Html.BeginForm() e Html.EndForm() (che fa la stessa cosa):

<% Html.BeginForm();  %>

   <fieldset>
   
      <!-- Fields Omitted for Brevity -->
   
      <p>
          <input type="submit" value="Save"/>
      </p>
   </fieldset>
   
<% Html.EndForm(); %>

La chiamata a Html.BeginForm() senza parametri causerà l'output di un elemento modulo che esegue un HTTP-POST all'URL della richiesta corrente. Questa è la ragione per cui la visualizzazione Modifica genera un'azione< modulo="/Dinners/Edit/1" method="post".> È possibile passare in alternativa parametri espliciti a Html.BeginForm() se si vuole pubblicare un URL diverso.

Metodo helper Html.TextBox()

La visualizzazione Edit.aspx usa il metodo helper Html.TextBox() per generare il tipo di input di output <="text"/> elements:

<%= Html.TextBox("Title") %>

Il metodo Html.TextBox() accetta un singolo parametro, che viene usato per specificare entrambi gli attributi id/name del <tipo di input="text"/> per l'output, nonché la proprietà del modello da cui popolare il valore della casella di testo. Ad esempio, l'oggetto Dinner passato alla visualizzazione Modifica ha un valore della proprietà "Title" di ".NET Futures" e quindi l'output della chiamata al metodo Html.TextBox("Title") viene chiamato: <input id="Title" name="Title" type="text" value=".NET Futures" />.

In alternativa, è possibile usare il primo parametro Html.TextBox() per specificare l'ID/nome dell'elemento e quindi passare in modo esplicito il valore da usare come secondo parametro:

<%= Html.TextBox("Title", Model.Title)%>

Spesso si vuole eseguire la formattazione personalizzata sul valore di output. Il metodo statico String.Format() predefinito in .NET è utile per questi scenari. Il modello di visualizzazione Edit.aspx usa questo metodo per formattare il valore EventDate (che è di tipo DateTime) in modo che non venga visualizzato secondi per il tempo:

<%= Html.TextBox("EventDate", String.Format("{0:g}", Model.EventDate)) %>

Un terzo parametro in Html.TextBox() può essere usato facoltativamente per restituire attributi HTML aggiuntivi. Il frammento di codice seguente illustra come eseguire il rendering di un attributo size="30" aggiuntivo e un attributo class="mycssclass" nel <tipo di input="text"/> elemento. Si noti come si esca il nome dell'attributo di classe usando un carattere "@" perché "class" è una parola chiave riservata in C#:

<%= Html.TextBox("Title", Model.Title, new { size=30, @class="myclass" } )%>

Implementazione del metodo di azione di modifica HTTP-POST

È ora disponibile la versione HTTP-GET del metodo Edit action implementato. Quando un utente richiede l'URL /Dinners/Edit/1 riceve una pagina HTML come segue:

Screenshot dell'output H T M L quando l'utente richiede una cena di modifica.

Premendo il pulsante "Salva" viene generato un post del modulo all'URL /Dinners/Edit/1 e invia i valori del modulo di input> HTML <usando il verbo HTTP POST. Ora implementiamo il comportamento HTTP POST del metodo di azione di modifica, che gestirà il salvataggio della cena.

Si inizierà aggiungendo un metodo di azione "Edit" in overload al nostro DinnersController con un attributo "AcceptVerbs" che indica che gestisce scenari HTTP POST:

//
// POST: /Dinners/Edit/2

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

Quando l'attributo [AcceptVerbs] viene applicato ai metodi di azione di overload, ASP.NET MVC gestisce automaticamente l'invio delle richieste al metodo di azione appropriato a seconda del verbo HTTP in ingresso. Le richieste HTTP POST a /Dinners/Edit/[id] URL passeranno al metodo Edit precedente, mentre tutte le altre richieste di verbo HTTP agli URL /Dinners/Edit/[id] passeranno al primo metodo Edit implementato (che non ha un [AcceptVerbs] attributo).

Argomento laterale: Perché distinguere tramite verbi HTTP?
È possibile chiedere: perché viene usato un singolo URL e differenziandone il comportamento tramite il verbo HTTP? Perché non sono disponibili solo due URL separati per gestire il caricamento e il salvataggio delle modifiche? Ad esempio: /Dinners/Edit/[id] per visualizzare il modulo iniziale e /Dinners/Save/[id] per gestire il post del modulo per salvarlo? Lo svantaggio della pubblicazione di due URL separati è che nei casi in cui si pubblica /Dinners/Save/2 e quindi è necessario riprodurre il modulo HTML a causa di un errore di input, l'utente finale finisce per avere l'URL /Dinners/Save/2 nella barra degli indirizzi del browser (poiché è stato l'URL del modulo pubblicato). Se l'utente finale aggiunge un segnalibro a questa pagina rieseguita all'elenco preferiti del browser o copia/incolla l'URL e lo invia tramite posta elettronica a un amico, finirà per salvare un URL che non funzionerà in futuro (poiché tale URL dipende dai valori di post). Esponendo un singolo URL (ad esempio: /Dinners/Edit/[id]) e differenziandone l'elaborazione in base al verbo HTTP, gli utenti finali possono aggiungere un segnalibro alla pagina di modifica e/o inviare l'URL ad altri utenti.

Recupero dei valori post modulo

Esistono diversi modi per accedere ai parametri del modulo pubblicati all'interno del metodo HTTP POST "Edit". Un approccio semplice consiste nell'usare solo la proprietà Request nella classe di base Controller per accedere alla raccolta moduli e recuperare direttamente i valori inseriti:

//
// POST: /Dinners/Edit/2

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

    // Retrieve existing dinner
    Dinner dinner = dinnerRepository.GetDinner(id);

    // Update dinner with form posted values
    dinner.Title = Request.Form["Title"];
    dinner.Description = Request.Form["Description"];
    dinner.EventDate = DateTime.Parse(Request.Form["EventDate"]);
    dinner.Address = Request.Form["Address"];
    dinner.Country = Request.Form["Country"];
    dinner.ContactPhone = Request.Form["ContactPhone"];

    // Persist changes back to database
    dinnerRepository.Save();

    // Perform HTTP redirect to details page for the saved Dinner
    return RedirectToAction("Details", new { id = dinner.DinnerID });
}

L'approccio precedente è un po' dettagliato, anche se, soprattutto dopo aver aggiunto la logica di gestione degli errori.

Un approccio migliore per questo scenario consiste nell'usare il metodo helper UpdateModel() predefinito nella classe di base Controller. Supporta l'aggiornamento delle proprietà di un oggetto che viene passato usando i parametri del modulo in ingresso. Usa la reflection per determinare i nomi delle proprietà nell'oggetto e quindi converte e assegna automaticamente i valori in base ai valori di input inviati dal client.

È possibile usare il metodo UpdateModel() per semplificare l'azione di modifica HTTP-POST usando questo codice:

//
// POST: /Dinners/Edit/2

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

    Dinner dinner = dinnerRepository.GetDinner(id);

    UpdateModel(dinner);

    dinnerRepository.Save();

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

È ora possibile visitare l'URL /Dinners/Edit/1 e modificare il titolo della cena:

Screenshot della pagina Modifica cena.

Quando si fa clic sul pulsante "Salva", si eseguirà un post di modulo all'azione Modifica e i valori aggiornati verranno salvati in modo permanente nel database. Verrà quindi eseguito il reindirizzamento all'URL dei dettagli per la cena (che visualizzerà i valori appena salvati):

Screenshot dell'URL dei dettagli per la cena.

Gestione degli errori di modifica

L'implementazione HTTP-POST corrente funziona correttamente, tranne quando si verificano errori.

Quando un utente commette un errore durante la modifica di un modulo, è necessario assicurarsi che il modulo venga rieseguito con un messaggio di errore informativo che li guida a risolverlo. Sono inclusi i casi in cui un utente finale pubblica input non corretto (ad esempio, una stringa di data in formato non valido), nonché casi in cui il formato di input è valido, ma si verifica una violazione della regola business. Quando si verificano errori, il modulo deve mantenere i dati di input immessi originariamente dall'utente in modo che non debbano riempire manualmente le modifiche. Questo processo deve essere ripetuto il maggior numero di volte necessario finché il modulo non viene completato correttamente.

ASP.NET MVC include alcune funzionalità predefinite che semplificano la gestione degli errori e la riproduzione dei moduli. Per visualizzare queste funzionalità in azione, è possibile aggiornare il metodo di azione Edit con il codice seguente:

//
// POST: /Dinners/Edit/2

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

    Dinner dinner = dinnerRepository.GetDinner(id);

    try {

        UpdateModel(dinner);

        dinnerRepository.Save();

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

        foreach (var issue in dinner.GetRuleViolations()) {
            ModelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
        }

        return View(dinner);
    }
}

Il codice precedente è simile all'implementazione precedente, ad eccezione del fatto che ora si esegue il wrapping di un blocco di gestione degli errori try/catch intorno al lavoro. Se si verifica un'eccezione quando si chiama UpdateModel() o quando si tenta di salvare DinnerRepository (che genererà un'eccezione se l'oggetto Dinner che si sta tentando di salvare non è valido a causa di una violazione della regola all'interno del modello), verrà eseguito il blocco di gestione degli errori catch. All'interno di esso si esegue qualsiasi violazione di regola presente nell'oggetto Dinner e le si aggiunge a un oggetto ModelState (che verrà illustrato a breve). Viene quindi visualizzata nuovamente la visualizzazione.

Per visualizzare il funzionamento, è possibile rieseguire l'applicazione, modificare una cena e modificarla in modo da avere un titolo vuoto, un EventDate di "BOGUS" e usare un numero di telefono del Regno Unito con un valore paese/area geografica degli Stati Uniti. Quando si preme il pulsante "Salva" il metodo HTTP POST Edit non sarà in grado di salvare la cena (perché ci sono errori) e verrà riprodotto il modulo:

Screenshot della riproduzione del modulo a causa di errori che usano il metodo H T T P S S T T Edit.

L'applicazione ha un'esperienza di errore decente. Gli elementi di testo con l'input non valido sono evidenziati in rosso e i messaggi di errore di convalida vengono visualizzati all'utente finale. Il modulo mantiene anche i dati di input immessi originariamente dall'utente, in modo che non sia necessario riempire nulla.

Come potresti chiederlo, questo si è verificato? In che modo le caselle di testo Title, EventDate e ContactPhone si evidenziano in rosso e sanno di restituire i valori utente originariamente immessi? E in che modo i messaggi di errore vengono visualizzati nell'elenco in alto? La buona notizia è che questo non si è verificato per magia, ma è stato perché sono state usate alcune delle funzionalità predefinite ASP.NET MVC che semplificano la convalida dell'input e gli scenari di gestione degli errori.

Informazioni sui metodi helper HTML di convalida e ModelState

Le classi controller dispongono di una raccolta di proprietà "ModelState" che consente di indicare che esistono errori con un oggetto modello passato a un oggetto View. Le voci di errore all'interno dell'insieme ModelState identificano il nome della proprietà del modello con il problema (ad esempio: "Title", "EventDate" o "ContactPhone") e consentono di specificare un messaggio di errore descrittivo, ad esempio "Title is required").

Il metodo helper UpdateModel() popola automaticamente l'insieme ModelState quando rileva errori durante il tentativo di assegnare valori del modulo alle proprietà nell'oggetto modello. Ad esempio, la proprietà EventDate dell'oggetto Dinner è di tipo DateTime. Quando il metodo UpdateModel() non è riuscito ad assegnarvi il valore stringa "BOGUS" nello scenario precedente, il metodo UpdateModel() ha aggiunto una voce all'insieme ModelState che indica che si è verificato un errore di assegnazione con tale proprietà.

Gli sviluppatori possono anche scrivere codice per aggiungere in modo esplicito le voci di errore nell'insieme ModelState, come nell'ambito del blocco di gestione degli errori "catch", che popola l'insieme ModelState con voci basate sulle violazioni delle regole attive nell'oggetto Dinner:

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

    Dinner dinner = dinnerRepository.GetDinner(id);

    try {
    
        UpdateModel(dinner);

        dinnerRepository.Save();

        return RedirectToAction("Details", new { id=dinner.DinnerID });
    }
    catch {
    
        foreach (var issue in dinner.GetRuleViolations()) {
            ModelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
        }

        return View(dinner);
    }
}

Integrazione dell'helper Html con ModelState

Metodi helper HTML, ad esempio Html.TextBox() , controllano la raccolta ModelState durante il rendering dell'output. Se esiste un errore per l'elemento, viene eseguito il rendering del valore immesso dall'utente e di una classe di errore CSS.

Ad esempio, nella visualizzazione "Edit" viene usato il metodo helper Html.TextBox() per eseguire il rendering di EventDate dell'oggetto Dinner:

<%= Html.TextBox("EventDate", String.Format("{0:g}", Model.EventDate)) %>

Quando è stato eseguito il rendering della visualizzazione nello scenario di errore, il metodo Html.TextBox() ha controllato l'insieme ModelState per verificare se si sono verificati errori associati alla proprietà "EventDate" dell'oggetto Dinner. Quando ha determinato che si è verificato un errore, ha eseguito il rendering dell'input utente inviato ("BOGUS") come valore e ha aggiunto una classe di errore css al <tipo di input="textbox"/> markup generato:

<input class="input-validation-error"id="EventDate" name="EventDate" type="text" value="BOGUS"/>

È possibile personalizzare l'aspetto della classe di errore css per l'aspetto desiderato. La classe di errore CSS predefinita , "input-validation-error", è definita nel foglio di stile \content\site.css e ha un aspetto simile al seguente:

.input-validation-error
{
    border: 1px solid #ff0000;
    background-color: #ffeeee;
}

Questa regola CSS ha causato l'evidenziata degli elementi di input non validi, come illustrato di seguito:

Screenshot degli elementi di input non validi evidenziati.

Metodo Helper Html.ValidationMessage()

Il metodo helper Html.ValidationMessage() può essere usato per restituire il messaggio di errore ModelState associato a una determinata proprietà del modello:

<%= Html.ValidationMessage("EventDate")%>

Output del codice precedente: <span class="field-validation-error"> Il valore 'BOGUS' non è valido</span>

Il metodo helper Html.ValidationMessage() supporta anche un secondo parametro che consente agli sviluppatori di eseguire l'override del messaggio di testo di errore visualizzato:

<%= Html.ValidationMessage("EventDate","*") %>

L'output del codice precedente: <span class="field-validation-error">*</span> anziché il testo di errore predefinito quando è presente un errore per la proprietà EventDate.

Metodo Helper Html.ValidationSummary()

Il metodo helper Html.ValidationSummary() può essere utilizzato per eseguire il rendering di un messaggio di errore di riepilogo, accompagnato da un <elenco ul><li/><ul> di tutti i messaggi di errore dettagliati nell'insieme ModelState:

Screenshot dell'elenco di tutti i messaggi di errore dettagliati nell'insieme ModelState.

Il metodo helper Html.ValidationSummary() accetta un parametro stringa facoltativo, che definisce un messaggio di errore di riepilogo da visualizzare sopra l'elenco di errori dettagliati:

<%= Html.ValidationSummary("Please correct the errors and try again.") %>

Facoltativamente, è possibile usare CSS per eseguire l'override dell'aspetto dell'elenco degli errori.

Uso di un metodo helper AddRuleViolations

L'implementazione iniziale della modifica HTTP-POST ha usato un'istruzione foreach all'interno del relativo blocco catch per eseguire il ciclo sulle violazioni delle regole dell'oggetto Dinner e aggiungerle all'insieme ModelState del controller:

catch {
        foreach (var issue in dinner.GetRuleViolations()) {
            ModelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
        }

        return View(dinner);
    }

È possibile rendere il codice un po' più pulito aggiungendo una classe "ControllerHelpers" al progetto NerdDinner e implementando un metodo di estensione "AddRuleViolations" all'interno di esso che aggiunge un metodo helper alla classe ASP.NET MVC ModelStateDictionary. Questo metodo di estensione può incapsulare la logica necessaria per popolare ModelStateDictionary con un elenco di errori RuleViolation:

public static class ControllerHelpers {

   public static void AddRuleViolations(this ModelStateDictionary modelState, IEnumerable<RuleViolation> errors) {
   
       foreach (RuleViolation issue in errors) {
           modelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
       }
   }
}

È quindi possibile aggiornare il metodo di azione HTTP-POST Edit per usare questo metodo di estensione per popolare l'insieme ModelState con le violazioni delle regole di cena.

Completare le implementazioni del metodo di azione di modifica

Il codice seguente implementa tutta la logica del controller necessaria per lo scenario di modifica:

//
// GET: /Dinners/Edit/2

public ActionResult Edit(int id) {

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

//
// POST: /Dinners/Edit/2

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

    Dinner dinner = dinnerRepository.GetDinner(id);

    try {
    
        UpdateModel(dinner);

        dinnerRepository.Save();

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

        return View(dinner);
    }
}

L'aspetto interessante dell'implementazione di Modifica è che né la classe Controller né il modello di visualizzazione deve sapere nulla sulla convalida specifica o le regole business applicate dal modello Dinner. È possibile aggiungere altre regole al modello in futuro e non è necessario apportare modifiche al codice al controller o alla visualizzazione per poterle supportare. Ciò offre la flessibilità necessaria per evolvere facilmente i requisiti dell'applicazione in futuro con modifiche minime al codice.

Creare il supporto

È stato completato l'implementazione del comportamento "Modifica" della classe DinnersController. Si procederà ora all'implementazione del supporto "Crea" su di esso, che consentirà agli utenti di aggiungere nuove cene.

Metodo di azione di creazione HTTP-GET

Si inizierà implementando il comportamento HTTP "GET" del metodo di creazione dell'azione. Questo metodo verrà chiamato quando un utente visita l'URL /Dinners/Create . L'implementazione è simile alla seguente:

//
// GET: /Dinners/Create

public ActionResult Create() {

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

    return View(dinner);
}

Il codice precedente crea un nuovo oggetto Dinner e assegna la proprietà EventDate a una settimana in futuro. Esegue quindi il rendering di un oggetto View basato sul nuovo oggetto Dinner. Poiché non è stato passato in modo esplicito un nome al metodo helper View(), verrà usato il percorso predefinito basato sulla convenzione per risolvere il modello di visualizzazione: /Views/Dinners/Create.aspx.

Verrà ora creato questo modello di visualizzazione. A tale scopo, fare clic con il pulsante destro del mouse all'interno del metodo Crea azione e scegliere il comando di menu di scelta rapida "Aggiungi visualizzazione". All'interno della finestra di dialogo "Aggiungi visualizzazione" si indicherà che si passa un oggetto Dinner al modello di visualizzazione e si sceglie di eseguire automaticamente lo scaffolding di un modello "Crea":

Screenshot di Aggiungi visualizzazione per creare un modello di visualizzazione.

Quando si fa clic sul pulsante "Aggiungi", Visual Studio salverà una nuova visualizzazione "Create.aspx" basata su scaffolding nella directory "\Views\Dinners" e la aprirà nell'IDE:

Screenshot dell'ID E per modificare il codice.

Verranno apportate alcune modifiche al file di scaffolding "create" predefinito generato per noi e modificarlo in modo che sia simile al seguente:

<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
     Host a Dinner
</asp:Content>

<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Host a Dinner</h2>

    <%=Html.ValidationSummary("Please correct the errors and try again.") %>
 
    <% using (Html.BeginForm()) {%>
  
        <fieldset>
            <p>
                <label for="Title">Title:</label>
                <%= Html.TextBox("Title") %>
                <%= Html.ValidationMessage("Title", "*") %>
            </p>
            <p>
                <label for="EventDate">EventDate:</label>
                <%=Html.TextBox("EventDate") %>
                <%=Html.ValidationMessage("EventDate", "*") %>
            </p>
            <p>
                <label for="Description">Description:</label>
                <%=Html.TextArea("Description") %>
                <%=Html.ValidationMessage("Description", "*") %>
            </p>
            <p>
                <label for="Address">Address:</label>
                <%=Html.TextBox("Address") %>
                <%=Html.ValidationMessage("Address", "*") %>
            </p>
            <p>
                <label for="Country">Country:</label>
                <%=Html.TextBox("Country") %>
                <%=Html.ValidationMessage("Country", "*") %>
            </p>
            <p>
                <label for="ContactPhone">ContactPhone:</label>
                <%=Html.TextBox("ContactPhone") %>
                <%=Html.ValidationMessage("ContactPhone", "*") %>
            </p>            
            <p>
                <label for="Latitude">Latitude:</label>
                <%=Html.TextBox("Latitude") %>
                <%=Html.ValidationMessage("Latitude", "*") %>
            </p>
            <p>
                <label for="Longitude">Longitude:</label>
                <%=Html.TextBox("Longitude") %>
                <%=Html.ValidationMessage("Longitude", "*") %>
            </p>
            <p>
                <input type="submit" value="Save"/>
            </p>
        </fieldset>
    <% } 
%>
</asp:Content>

E ora quando si esegue l'applicazione e si accede all'URL "/Dinners/Create" all'interno del browser, verrà eseguito il rendering dell'interfaccia utente come illustrato di seguito dall'implementazione crea azione:

Screenshot dell'implementazione di Crea azione quando si esegue l'applicazione e si accede a Dinners U R L.

Implementazione del metodo di azione di creazione HTTP-POST

È disponibile la versione HTTP-GET del metodo Create action implementata. Quando un utente fa clic sul pulsante "Salva" esegue un post del modulo all'URL /Dinners/Create e invia i valori del modulo di input> HTML <usando il verbo HTTP POST.

Verrà ora implementato il comportamento HTTP POST del metodo di azione di creazione. Si inizierà aggiungendo un metodo di azione "Create" in overload al nostro DinnersController con un attributo "AcceptVerbs" che indica che gestisce scenari HTTP POST:

//
// POST: /Dinners/Create

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

Esistono diversi modi per accedere ai parametri del modulo pubblicati all'interno del metodo "Create" abilitato per HTTP-POST.

Un approccio consiste nel creare un nuovo oggetto Dinner e quindi usare il metodo helper UpdateModel() (come abbiamo fatto con l'azione Modifica) per popolarlo con i valori del modulo pubblicati. È quindi possibile aggiungerlo alla cenaRepository, renderlo persistente nel database e reindirizzare l'utente all'azione Dettagli per visualizzare la nuova cena creata usando il codice seguente:

//
// POST: /Dinners/Create

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

    Dinner dinner = new Dinner();

    try {
    
        UpdateModel(dinner);

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

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

        return View(dinner);
    }
}

In alternativa, è possibile usare un approccio in cui è disponibile il metodo di azione Create() accetta un oggetto Dinner come parametro del metodo. ASP.NET MVC crea automaticamente un'istanza di un nuovo oggetto Dinner per noi, popola le relative proprietà usando gli input del modulo e lo passa al metodo di azione:

//
//
// 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.AddRuleViolations(dinner.GetRuleViolations());
        }
    }
    
    return View(dinner);
}

Il metodo di azione precedente verifica che l'oggetto Dinner sia stato popolato correttamente con i valori dei post del modulo controllando la proprietà ModelState.IsValid. Verrà restituito false se sono presenti problemi di conversione di input, ad esempio una stringa di "BOGUS" per la proprietà EventDate e, se si verificano problemi, il metodo di azione esegue nuovamente la riproduzione del modulo.

Se i valori di input sono validi, il metodo azione tenta di aggiungere e salvare la nuova cena in DinnerRepository. Esegue il wrapping di questo lavoro all'interno di un blocco try/catch e redisplay il modulo se sono presenti violazioni delle regole aziendali (che causerebbe il metodo dinnerRepository.Save() per generare un'eccezione.

Per visualizzare questo comportamento di gestione degli errori in azione, è possibile richiedere /Dinners/Create URL e compilare i dettagli relativi a una nuova cena. L'input o i valori non corretti causano la riproduzione del modulo di creazione con gli errori evidenziati come indicato di seguito:

Screenshot del modulo redisplayed con errori evidenziati.

Si noti come il modulo Crea rispetta le stesse regole di convalida e business del modulo Di modifica. Ciò è dovuto al fatto che le regole di convalida e business sono state definite nel modello e non sono state incorporate nell'interfaccia utente o nel controller dell'applicazione. Ciò significa che è possibile modificare/evolvere le regole di convalida o business in un'unica posizione e applicarle in tutta l'applicazione. Non sarà necessario modificare alcun codice all'interno dei metodi di modifica o creazione di azioni per rispettare automaticamente le nuove regole o le modifiche a quelle esistenti.

Quando si correzionono i valori di input e si fa di nuovo clic sul pulsante "Salva", l'aggiunta alla CenaRepository avrà esito positivo e verrà aggiunta una nuova cena al database. Verrà quindi reindirizzato all'URL /Dinners/Details/[id] in cui verranno presentati i dettagli relativi alla nuova cena creata:

Screenshot della nuova cena creata.

Elimina supporto

Aggiungiamo ora il supporto "Elimina" al nostro DinnersController.

Metodo di azione di eliminazione HTTP-GET

Si inizierà implementando il comportamento HTTP GET del metodo di azione di eliminazione. Questo metodo verrà chiamato quando un utente visita l'URL /Dinners/Delete/[id]. Di seguito è riportata l'implementazione:

//
// HTTP GET: /Dinners/Delete/1

public ActionResult Delete(int id) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    if (dinner == null)
         return View("NotFound");
    else
        return View(dinner);
}

Il metodo di azione tenta di recuperare la cena da eliminare. Se la cena esiste, esegue il rendering di una visualizzazione in base all'oggetto Dinner. Se l'oggetto non esiste (o è già stato eliminato) restituisce una visualizzazione che esegue il rendering del modello di visualizzazione "NotFound" creato in precedenza per il metodo di azione "Dettagli".

È possibile creare il modello di visualizzazione "Elimina" facendo clic con il pulsante destro del mouse sul metodo di azione Delete e selezionando il comando di menu di scelta rapida "Aggiungi visualizzazione". Nella finestra di dialogo "Aggiungi visualizzazione" verrà indicato che si passa un oggetto Dinner al modello di visualizzazione come modello e si sceglie di creare un modello vuoto:

Screenshot della creazione del modello di visualizzazione Elimina come modello vuoto.

Quando si fa clic sul pulsante "Aggiungi", Visual Studio aggiungerà un nuovo file di modello di visualizzazione "Delete.aspx" all'interno della directory "\Views\Dinners". Si aggiungeranno alcuni codice HTML e codice al modello per implementare una schermata di conferma di eliminazione come illustrato di seguito:

<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
    Delete Confirmation:  <%=Html.Encode(Model.Title) %>
</asp:Content>

<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">

    <h2>
        Delete Confirmation
    </h2>

    <div>
        <p>Please confirm you want to cancel the dinner titled: 
           <i> <%=Html.Encode(Model.Title) %>? </i> 
        </p>
    </div>
    
    <% using (Html.BeginForm()) {  %>
        <input name="confirmButton" type="submit" value="Delete" />        
    <% } %>
     
</asp:Content>

Il codice precedente visualizza il titolo della cena da eliminare e restituisce un elemento modulo> che esegue un <POST all'URL /Dinners/Delete/[id] se l'utente finale fa clic sul pulsante "Elimina".

Quando si esegue l'applicazione e si accede all'URL "/Dinners/Delete/[id]" per un oggetto Dinner valido che esegue il rendering dell'interfaccia utente come indicato di seguito:

Screenshot dell'operazione di conferma dell'eliminazione della cena U nel metodo di azione H T T G E T Delete.

Argomento laterale: perché si sta facendo un POST?
Potresti chiedere: perché abbiamo eseguito lo sforzo di creare un <modulo> all'interno della schermata di conferma dell'eliminazione? Perché non usare solo un collegamento ipertestuale standard per collegare a un metodo di azione che esegue l'operazione di eliminazione effettiva? Il motivo è dovuto al fatto che si vuole prestare attenzione ai web-crawler e ai motori di ricerca che individuano gli URL e causano inavvertitamente l'eliminazione dei dati quando seguono i collegamenti. Gli URL basati su HTTP-GET sono considerati "sicuri" per accedervi/eseguire la ricerca per indicizzazione e dovrebbero non seguire quelli HTTP-POST. Una buona regola consiste nel assicurarsi di inserire sempre operazioni distruttive o di modifica dei dati dietro le richieste HTTP-POST.

Implementazione del metodo di azione di eliminazione HTTP-POST

È ora disponibile la versione HTTP-GET del metodo di azione Delete implementata che visualizza una schermata di conferma dell'eliminazione. Quando un utente finale fa clic sul pulsante "Elimina" eseguirà un post del modulo all'URL /Dinners/Dinner/[id].

Verrà ora implementato il comportamento HTTP "POST" del metodo di azione di eliminazione usando il codice seguente:

// 
// HTTP POST: /Dinners/Delete/1

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Delete(int id, string confirmButton) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    if (dinner == null)
        return View("NotFound");

    dinnerRepository.Delete(dinner);
    dinnerRepository.Save();

    return View("Deleted");
}

La versione HTTP-POST del metodo di azione Delete tenta di recuperare l'oggetto dinner da eliminare. Se non riesce a trovarlo (perché è già stato eliminato) esegue il rendering del modello "NotFound". Se trova la cena, la elimina dalla CenaRepository. Esegue quindi il rendering di un modello "Eliminato".

Per implementare il modello "Eliminato", fare clic con il pulsante destro del mouse nel metodo di azione e scegliere il menu di scelta rapida "Aggiungi visualizzazione". Verrà denominata la visualizzazione "Eliminata" e sarà un modello vuoto (e non prendere un oggetto modello fortemente tipizzato). Si aggiungerà quindi un contenuto HTML al contenuto:

<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
    Dinner Deleted
</asp:Content>

<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Dinner Deleted</h2>

    <div>
        <p>Your dinner was successfully deleted.</p>
    </div>
    
    <div>
        <p><a href="/dinners">Click for Upcoming Dinners</a></p>
    </div>
    
</asp:Content>

E ora quando si esegue l'applicazione e si accede all'URL "/Dinners/Delete/[id]" per un oggetto Dinner valido, verrà visualizzata la schermata di conferma dell'eliminazione della cena come illustrato di seguito:

Screenshot della schermata di conferma dell'eliminazione della cena nel metodo di azione H T P P O T Delete.

Quando si fa clic sul pulsante "Elimina" verrà eseguito un HTTP-POST all'URL /Dinners/Delete/[id] che eliminerà la cena dal database e visualizzerà il modello di visualizzazione "Eliminato":

Screenshot del modello di visualizzazione eliminato.

Sicurezza associazione modelli

Sono stati illustrati due modi diversi per usare le funzionalità di associazione di modelli predefinite di ASP.NET MVC. Il primo uso del metodo UpdateModel() per aggiornare le proprietà in un oggetto modello esistente e il secondo usando il supporto di ASP.NET MVC per passare oggetti modello in come parametri del metodo di azione. Entrambe queste tecniche sono molto potenti e estremamente utili.

Questo potere comporta anche la responsabilità. È importante essere sempre paranoidi sulla sicurezza quando si accetta qualsiasi input utente e questo è anche vero quando si associano oggetti per formare l'input. È consigliabile prestare attenzione a codificare sempre i valori immessi dall'utente per evitare attacchi di inserimento HTML e JavaScript e prestare attenzione agli attacchi sql injection (nota: si usa LINQ to SQL per l'applicazione, che codifica automaticamente i parametri per impedire questi tipi di attacchi). Non è mai consigliabile basarsi sulla convalida lato client e usare sempre la convalida lato server per proteggere gli hacker che tentano di inviare valori fittizi.

Un elemento di sicurezza aggiuntivo per assicurarsi di usare le funzionalità di associazione di ASP.NET MVC è l'ambito degli oggetti che si sta associando. In particolare, si vuole assicurarsi di comprendere le implicazioni di sicurezza delle proprietà che si consentono di associare e assicurarsi di consentire l'aggiornamento di tali proprietà che devono essere effettivamente aggiornabili da parte di un utente finale.

Per impostazione predefinita, il metodo UpdateModel() tenterà di aggiornare tutte le proprietà nell'oggetto modello che corrispondono ai valori dei parametri del modulo in ingresso. Analogamente, gli oggetti passati come parametri del metodo di azione possono anche avere tutte le relative proprietà impostate tramite parametri di modulo.

Blocco dell'associazione in base all'utilizzo

È possibile bloccare i criteri di associazione in base all'utilizzo fornendo un "elenco di inclusione" esplicito delle proprietà che possono essere aggiornate. Questa operazione può essere eseguita passando un parametro di matrice stringa aggiuntivo al metodo UpdateModel() come indicato di seguito:

string[] allowedProperties = new[]{ "Title","Description", 
                                    "ContactPhone", "Address",
                                    "EventDate", "Latitude", 
                                    "Longitude"};
                                    
UpdateModel(dinner, allowedProperties);

Gli oggetti passati come parametri del metodo di azione supportano anche un attributo [Bind] che consente di specificare un "elenco di inclusioni" di proprietà consentite come indicato di seguito:

//
// POST: /Dinners/Create

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create( [Bind(Include="Title,Address")] Dinner dinner ) {
    ...
}

Blocco dell'associazione in base a un tipo

È anche possibile bloccare le regole di associazione in base al tipo. In questo modo è possibile specificare le regole di associazione una sola volta e quindi applicarle in tutti gli scenari (inclusi gli scenari dei parametri del metodo UpdateModel e action) in tutti i controller e i metodi di azione.

È possibile personalizzare le regole di associazione per tipo aggiungendo un attributo [Bind] a un tipo oppure registrandolo all'interno del file Global.asax dell'applicazione (utile per gli scenari in cui non è proprietario del tipo). È quindi possibile usare le proprietà Include ed Exclude dell'attributo Bind per controllare quali proprietà sono associabili per la classe o l'interfaccia specifica.

Questa tecnica verrà usata per la classe Dinner nell'applicazione NerdDinner e si aggiungerà un attributo [Bind] che limita l'elenco di proprietà associabili ai seguenti:

[Bind(Include="Title,Description,EventDate,Address,Country,ContactPhone,Latitude,Longitude")]
public partial class Dinner {
   ...
}

Si noti che non è possibile modificare l'insieme RSVPs tramite l'associazione, né consentire l'impostazione delle proprietà DinnerID o HostedBy tramite l'associazione. Per motivi di sicurezza verranno invece modificate solo queste particolari proprietà usando codice esplicito all'interno dei metodi di azione.

Wrap-Up CRUD

ASP.NET MVC include numerose funzionalità predefinite che consentono di implementare scenari di registrazione dei moduli. È stata usata una varietà di queste funzionalità per fornire supporto dell'interfaccia utente CRUD in cima alla nostra CenaRepository.

Si usa un approccio incentrato sul modello per implementare l'applicazione. Ciò significa che tutta la logica di convalida e regola di business viene definita all'interno del livello del modello e non all'interno dei controller o delle visualizzazioni. Né la classe Controller né i modelli Di visualizzazione sanno nulla sulle regole business specifiche applicate dalla classe modello Dinner.

In questo modo l'architettura dell'applicazione sarà pulita e semplifica il test. È possibile aggiungere regole business aggiuntive al livello del modello in futuro e non è necessario apportare modifiche al codice al controller o alla visualizzazione per poterle supportare. Ciò ci fornirà una grande flessibilità per evolvere e modificare l'applicazione in futuro.

La nostra CenasController ora abilita elenchi di cena/dettagli, nonché creare, modificare ed eliminare il supporto. Il codice completo per la classe è disponibile di seguito:

public class DinnersController : Controller {

    DinnerRepository dinnerRepository = new DinnerRepository();

    //
    // GET: /Dinners/

    public ActionResult Index() {

        var dinners = dinnerRepository.FindUpcomingDinners().ToList();
        return View(dinners);
    }

    //
    // GET: /Dinners/Details/2

    public ActionResult Details(int id) {

        Dinner dinner = dinnerRepository.GetDinner(id);

        if (dinner == null)
            return View("NotFound");
        else
            return View(dinner);
    }

    //
    // GET: /Dinners/Edit/2

    public ActionResult Edit(int id) {

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

    //
    // POST: /Dinners/Edit/2

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

        Dinner dinner = dinnerRepository.GetDinner(id);

        try {
            UpdateModel(dinner);

            dinnerRepository.Save();

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

            return View(dinner);
        }
    }

    //
    // GET: /Dinners/Create

    public ActionResult Create() {

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

    //
    // 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.AddRuleViolations(dinner.GetRuleViolations());
            }
        }

        return View(dinner);
    }

    //
    // HTTP GET: /Dinners/Delete/1

    public ActionResult Delete(int id) {

        Dinner dinner = dinnerRepository.GetDinner(id);

        if (dinner == null)
            return View("NotFound");
        else
            return View(dinner);
    }

    // 
    // HTTP POST: /Dinners/Delete/1

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Delete(int id, string confirmButton) {

        Dinner dinner = dinnerRepository.GetDinner(id);

        if (dinner == null)
            return View("NotFound");

        dinnerRepository.Delete(dinner);
        dinnerRepository.Save();

        return View("Deleted");
    }
}

passaggio successivo

È ora disponibile il supporto CRUD di base (Create, Read, Update and Delete) all'interno della classe DinnersController.

Verrà ora illustrato come usare le classi ViewData e ViewModel per abilitare un'interfaccia utente ancora più ricca nei moduli.