Skip to content

4. Il modello di un'azione

Torniamo all'architettura di un'applicazione ASP.NET MVC:

Nel capitolo precedente abbiamo esaminato il processo che instrada la richiesta [1] al controller e all'azione [2a] che la gestiranno, un meccanismo noto come routing. Abbiamo anche presentato le varie risposte che un'azione può inviare al browser. Finora abbiamo presentato azioni che non elaboravano la richiesta loro sottoposta. Una richiesta [1] trasporta varie informazioni che ASP.NET MVC presenta [2a] all'azione sotto forma di un modello. Questo termine non deve essere confuso con il modello M di una vista V [2c] prodotta dall'azione:

  • la richiesta HTTP del client arriva a [1];
  • in [2], le informazioni contenute nella richiesta vengono trasformate in un modello di azione [3], spesso ma non necessariamente una classe, che funge da input per l'azione [4];
  • in [4], l'azione, sulla base di questo modello, genera una risposta. Questa risposta ha due componenti: una vista V [6] e il modello M di questa vista [5];
  • la vista V [6] utilizzerà il proprio modello M [5] per generare la risposta HTTP destinata al client.

Nel modello MVC, l'azione [4] fa parte del C (controller), il modello della vista [5] è l'M e la vista [6] è la V.

Questo capitolo esamina i meccanismi per collegare le informazioni trasportate dalla richiesta — che sono intrinsecamente stringhe — al modello dell’azione, che può essere una classe con proprietà di vario tipo.

4.1. Inizializzazione dei parametri dell'azione

Aggiungiamo [1] un nuovo progetto ASP.NET MVC di base alla soluzione esistente:

  • in [2], il nome del nuovo progetto;
  • in [3, 4], selezioniamo un progetto ASP.NET MVC di base;
  • in [5], il nuovo progetto.

Impostiamo il nuovo progetto come progetto di avvio per la soluzione.

Come fatto nella Sezione 3.1, creiamo un controller denominato [First] [1]:

Image

In questo controller, creiamo la seguente azione [Action01]:


using System.Web.Mvc;
 
namespace Exemple_02.Controllers
{
  public class FirstController : Controller
  {
    // Action01
    public ContentResult Action01(string nom)
    {
      return Content(string.Format("Contrôleur=First, Action=Action01, nom={0}", nom));
    }
 
  }
}

La novità si trova alla riga 8: il metodo [Action01] ha un parametro. In questo capitolo esploreremo i diversi modi per inizializzare i parametri di un'azione. Il parametro [name] sopra riportato viene inizializzato in ordine con i seguenti valori:

Request.Form["name"]
un parametro denominato [name] inviato da una richiesta POST
RouteData.Values["name"]
un elemento URL denominato [name]
Request.QueryString["name"]
un parametro denominato [name] inviato da una richiesta GET
Request.Files["name"]
un file caricato denominato [name]

Esaminiamo questi diversi casi. Inseriamo l'URL [/First/Action01?name=someone] direttamente nel browser. Otteniamo la seguente risposta:

Image

La richiesta HTTP del browser era la seguente:

1
2
3
GET /First/Action01?nom=someone HTTP/1.1
Host: localhost:55483
...
  • Riga 1: La richiesta è di tipo GET. L'URL richiesto include il parametro [name]. Sul lato server, la richiesta viene instradata all'azione [Action01], che ha la seguente firma:

public ContentResult Action01(string nom)

Per assegnare un valore al parametro name, ASP.NET MVC controlla i seguenti valori in ordine: *Request.Form[&quot;name&quot;], RouteData.Values[&quot;name&quot;],* *<span style="color: #2323dc">Request.QueryString[&quot;name&quot;]</span>**, Request.Files[&quot;name&quot;]*. Si ferma non appena trova un valore. Il parametro [name] incorporato nell'URL GET è stato inserito dal framework in Request.QueryString["name"]. È con questo valore [someone] che verrà inizializzato il parametro [name] di [Action01]. Quindi viene eseguito il codice di [Action01]:


return Content(string.Format("Contrôleur=First, Action=Action01, nom={0}", nom), "text/plain", Encoding.UTF8);

Questo codice fornisce la risposta inviata al client:

Image

Nota: il meccanismo di associazione dei parametri non distingue tra maiuscole e minuscole. Quindi, se la nostra azione è definita come:


public ContentResult Action01(string NOM)

e il parametro passato è [?Name=zébulon], il binding avverrà comunque. Il parametro [Name] di [Action01] riceverà il valore [zébulon].

Ora, richiediamo lo stesso URL con un POST. Per farlo, useremo l'applicazione [Advanced Rest Client]:

  • in [1], l'URL richiesto;
  • in [2], verrà utilizzato il metodo POST;
  • in [3], i parametri POST.

Inviamo questa richiesta e diamo un'occhiata ai log HTTP. La richiesta HTTP è la seguente:

  • in [1], il POST;
  • in [2], i parametri POST. Tecnicamente, sono stati inviati dopo le intestazioni HTTP, a seguire la riga vuota che indica la fine di tali intestazioni;
  • in [3], la risposta ricevuta. Abbiamo recuperato con successo il parametro [name] dal POST. Tra i valori testati per il parametro name Request.Form["name"], RouteData.Values["name"], Request.QueryString["name"], Request.Files["name"] — il primo ha funzionato.

Ora, modifichiamo il percorso predefinito in [App_Start/RouteConfig]. Attualmente, questo percorso è il seguente:


      routes.MapRoute(
          name: "Default",
          url: "{controller}/{action}/{id}",
          defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

Modifichiamolo in:


      routes.MapRoute(
          name: "Default",
          url: "{controller}/{action}/{nom}",
          defaults: new { controller = "Home", action = "Index", nom = UrlParameter.Optional }
);

  • Nella riga 3, abbiamo denominato il terzo elemento di una route [name];
  • alla riga 4, questo elemento è dichiarato opzionale.

Ora ricompiliamo l'applicazione e richiediamo l'URL [/First/Action01/zébulon] direttamente nel browser. Otteniamo la seguente risposta:

Image

Tra i valori testati per il parametro "name" — Request.Form["name"], RouteData.Values["name"], Request.QueryString["name"], Request.Files["name"] — il secondo ha funzionato.

Effettuiamo la stessa richiesta utilizzando una richiesta POST e [Advanced Rest Client]:

  • In [1], abbiamo assegnato un valore all'elemento {name} del percorso;
  • in [2], aggiungiamo un parametro [name] alla richiesta inviata;
  • la risposta ottenuta è in [3].

Tra i valori testati per il parametro [name] Request.Form["name"], RouteData.Values["name"], Request.QueryString["name"], Request.Files["name"] — due erano validi: i primi due. È stato utilizzato il primo.

4.2. Convalida dei parametri dell'azione

Se un'azione presenta un parametro denominato [p], ASP.NET MVC tenterà di assegnargli uno dei seguenti valori: Request.Form["p"], RouteData.Values["p"], Request.QueryString["p"] o Request.Files["p"]. I primi tre valori sono stringhe. Se il parametro [p] non è di tipo [string], potrebbero verificarsi dei problemi.

Creiamo la seguente nuova azione:


    // Action02
    public ContentResult Action02(int age)
    {
      string texte = string.Format("Contrôleur={0}, Action={1}, âge={2}", RouteData.Values["controller"], RouteData.Values["action"],age);
      return Content(texte, "text/plain", Encoding.UTF8);
}

  • Riga 2: L'azione [Action02] accetta un parametro denominato [age] di tipo int. La stringa recuperata deve essere convertibile in int.

Richiediamo l'URL [http://localhost:55483/First/Action02?age=21]. Otteniamo la seguente pagina:

Image

Richiediamo l'URL [http://localhost:55483/First/Action02?age=21x]. Otteniamo la seguente pagina:

Image

Questa volta abbiamo ricevuto una pagina di errore. È interessante osservare le intestazioni HTTP inviate dal server in questo caso:

1
2
3
4
5
HTTP/1.1 500 Internal Server Error
...
Content-Type: text/html; charset=utf-8
...
Content-Length: 12438

  • Riga 1: Il server ha risposto con un codice [500 Internal Server Error] e ha inviato una pagina HTML (riga 3) di 12.438 byte (riga 5) per spiegare le possibili cause di questo errore.

Ora creiamo la seguente azione [Action03]:


    // Action03
    public ContentResult Action03(int? age)
    {
      ...
}

[Action03] è identica a [Action02], tranne per il fatto che abbiamo cambiato il tipo del parametro [age] in int?, che significa intero o null.

Richiediamo l'URL [http://localhost:55483/First/Action03?age=21x]. Otteniamo la seguente pagina:

Image

ASP.NET MVC non è riuscito a convertire [21x] in un tipo int. Ha quindi assegnato il valore null al parametro [age], come consentito dal suo tipo int?. Tuttavia, è possibile determinare se il parametro ha ricevuto un valore dalla richiesta o meno.

Creiamo la seguente nuova azione [Action04]:


    // Action04
    public ContentResult Action04(int? age)
    {
      bool valide = ModelState.IsValid;
      string texte = string.Format("Contrôleur={0}, Action={1}, âge={2}, valide={3}", RouteData.Values["controller"], RouteData.Values["action"], age, valide);
      return Content(texte, "text/plain", Encoding.UTF8);
}

  • Riga 2: Abbiamo mantenuto il tipo [int?]. Ciò consente specificamente alla richiesta di omettere il parametro [age], che riceve quindi il valore null;
  • riga 4: verifichiamo se il modello dell'azione è valido. Il modello dell'azione è costituito da tutti i suoi parametri, in questo caso [age]. Il modello è valido se tutti i parametri sono riusciti a ottenere un valore dalla richiesta o il valore null se il tipo di parametro lo consente;
  • Riga 5: Aggiungiamo il valore della variabile [valid] al testo inviato al client.

Richiediamo l'URL [http://localhost:55483/First/Action04?age=21x]. Otteniamo la seguente pagina:

Image

ASP.NET MVC non è riuscito a convertire [21x] nel tipo int. Ha quindi assegnato il valore null al parametro [age], come consentito dal suo tipo int?. Tuttavia, si sono verificati errori di conversione, come indicato dal valore di [valid].

È possibile ottenere il messaggio di errore associato a una conversione non riuscita. Esaminiamo la seguente nuova azione:


    // Action05
    public ContentResult Action05(int? age)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("Contrôleur={0}, Action={1}, âge={2}, valide={3}, erreurs={4}", RouteData.Values["controller"], RouteData.Values["action"], age, ModelState.IsValid, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

La nuova funzionalità si trova alla riga 4. Qui chiamiamo un metodo privato [getErrorMessagesFor] e gli passiamo lo stato del modello dell'azione. Esso restituisce una stringa contenente tutti i messaggi di errore verificatisi. Questo metodo è il seguente:


private string getErrorMessagesFor(ModelStateDictionary état)
    {
      List<String> erreurs = new List<String>();
      string messages = string.Empty;
      if (!état.IsValid)
      {
        foreach (ModelState modelState in état.Values)
        {
          foreach (ModelError error in modelState.Errors)
          {
            erreurs.Add(getErrorMessageFor(error));
          }
        }
        foreach (string message in erreurs)
        {
          messages += string.Format("[{0}]", message);
        }
      }
      return messages;
    }

  • riga 1: il parametro [ModelState] effettivamente passato al metodo è di tipo [ModelStateDictionary];
  • riga 3: un elenco di messaggi di errore, inizialmente vuoto;
  • riga 5: verifichiamo se lo stato passato come parametro è valido o meno. In caso contrario, aggregheremo tutti i messaggi di errore in un'unica stringa;
  • riga 7: il tipo [ModelStateDictionary] ha una proprietà [Values], che è una raccolta di tipi [ModelState]. C'è un [ModelState] per ogni elemento del modello. Ad esempio:

    • ModelState["age"]: lo stato del modello dell'azione per il parametro [age],
    • ModelState["age"].Errors: la raccolta di errori per questo parametro. Gli errori sono di tipo [ModelError],
    • ModelState["age"].Errors[i].ErrorMessage: il messaggio di errore (se presente) per il parametro [age] del modello
    • ModelState["age"].Errors[i].Exception: l'eccezione per l'errore n. i nella raccolta di errori per il parametro [age],
    • ModelState["age"].Errors[i].Exception.InnerException: la causa di questa eccezione,
    • ModelState["age"].Errors[i].Exception.InnerException.Message: il messaggio relativo alla causa dell'eccezione;
  • riga 9: si esegue l'iterazione attraverso la raccolta [Errors] di uno specifico [ModelState];
  • riga 11: recuperiamo il messaggio di errore da uno specifico [ModelError] e lo aggiungiamo all'elenco dei messaggi di errore della riga 3;
  • righe 14–17: gli elementi dell'elenco dei messaggi di errore vengono concatenati in un'unica stringa.

Il metodo [getErrorMessageFor] alla riga 11 è il seguente:


    private string getErrorMessageFor(ModelError error)
    {
      if (error.ErrorMessage != null && error.ErrorMessage.Trim() != string.Empty)
      {
        return error.ErrorMessage;
      }
      if (error.Exception != null && error.Exception.InnerException == null && error.Exception.Message != string.Empty)
      {
        return error.Exception.Message;
      }
      if (error.Exception != null && error.Exception.InnerException != null && error.Exception.InnerException.Message != string.Empty)
      {
        return error.Exception.InnerException.Message;
      }
      return string.Empty;
}

  • Riga 1: Riceviamo un tipo [ModelError] che incapsula un errore su uno degli elementi del modello dell'azione. Recuperiamo il messaggio di errore da tre diverse posizioni:

    • in [ModelError].ErrorMessage, righe 3–6;
    • in [ModelError].Exception.Message, righe 7–10;
    • in [ModelError].Exception.InnerException.Message, righe 11–14;

Durante il test, notiamo che il messaggio di errore si trova in queste tre posizioni a seconda della natura dell'elemento del modello. Deve esserci una regola che garantisca la possibilità di ottenere il messaggio di errore associato a un elemento del modello, ma non la conosco. Quindi lo cerco nei vari posti in cui posso trovarlo, seguendo un ordine specifico. Non appena viene trovato un messaggio non vuoto, viene restituito.

Richiediamo l'URL [http://localhost:55483/First/Action05?age=21x]. Otteniamo la seguente pagina:

Image

4.3. Un'azione con più parametri

Consideriamo la seguente nuova azione:


    // Action06
    public ContentResult Action06(double? poids, int? age)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("Contrôleur={0}, Action={1}, poids={2}, âge={3}, valide={4}, erreurs={5}", RouteData.Values["controller"], RouteData.Values["action"], poids, age, ModelState.IsValid, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

  • Riga 2: Abbiamo due parametri [peso] e [età].

Le regole descritte sopra si applicano ora a entrambi i parametri. Ecco alcuni esempi di esecuzione:

Image

Image

4.4. Utilizzo di una classe come modello di azione

Definiamo una classe che fungerà da modello per un'azione. La inseriremo nella cartella [Models] [1].

Il suo codice sarà il seguente:


namespace Exemple_02.Models
{
  public class ActionModel01
  {
    public double? Poids { get; set; }
    public int? Age { get; set; }
  }
}

La nostra classe ha come proprietà automatiche i due parametri [Peso] ed [Età] discussi in precedenza. Questa classe sarà il parametro di input per l'azione [Action07]:


    // Action07
    public ContentResult Action07(ActionModel01 modèle)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("Contrôleur={0}, Action={1}, poids={2}, âge={3}, valide={4}, erreurs={5}", RouteData.Values["controller"], RouteData.Values["action"], modèle.Poids, modèle.Age, ModelState.IsValid, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

  • Riga 2: Il modello di azione è un'istanza di tipo [ActionModel01].

Riprendiamo gli stessi due esempi di prima:

Image

Image

Si noti che il binding dei parametri non distingue tra maiuscole e minuscole. I parametri della richiesta erano [age] e [weight]. Essi hanno popolato le proprietà [Age] e [Weight] della classe [ModelAction01].

Inoltre, finora abbiamo utilizzato richieste HTTP [GET]. Dimostriamo che le richieste [POST] si comportano allo stesso modo. Per farlo, utilizziamo nuovamente l'applicazione [Advanced Rest Client]:

  • in [1], l'URL richiesto;
  • in [2], verrà inviata tramite una richiesta POST;
  • in [3], i parametri POST.

Otteniamo la stessa risposta della richiesta GET:

Image

4.5. Modello di azione con vincoli di convalida - 1

Utilizzando il modello precedente:


namespace Exemple_02.Models
{
  public class ActionModel01
  {
    public double? Poids { get; set; }
    public int? Age { get; set; }
  }
}

I parametri [weight] e [age] possono essere omessi dalla richiesta. In questo caso, le proprietà [Weight] e [Age] vengono impostate su [null] e non viene segnalato alcun errore. Potresti voler trasformare il modello come segue:


namespace Exemple_02.Models
{
  public class ActionModel01
  {
    public double Poids { get; set; }
    public int Age { get; set; }
  }
}

Righe 5 e 6: le proprietà [Weight] e [Age] non possono più assumere il valore [null]. Vediamo cosa succede con questo nuovo modello quando i parametri [weight] e [age] mancano dalla richiesta.

Image

Non si sono verificati errori e le proprietà [Weight] e [Age] hanno mantenuto il loro valore di inizializzazione: 0. ASP.NET MVC:

  • ha creato un'istanza del modello utilizzando `new ActionModel01`. È qui che alle proprietà [Weight] e [Age] è stato assegnato il valore 0;
  • non ha assegnato alcun valore a queste due proprietà perché non c'erano parametri con quei nomi.

Il primo modello ci permette di verificare l'assenza di un parametro: la proprietà corrispondente avrà quindi il valore [null]. Il secondo non lo permette. È possibile aggiungere vincoli di validazione che vanno oltre il semplice tipo dei parametri. Li presenteremo ora.

Consideriamo il seguente nuovo modello di azione:

Image


using System.ComponentModel.DataAnnotations;
namespace Exemple_02.Models
{
  public class ActionModel02
  {
    [Required]
    [Range(1, 200)]
    public double? Poids { get; set; }
    [Required]
    [Range(1, 150)]
    public int? Age { get; set; }
  }
}

  • riga 6: indica che il campo [Weight] è obbligatorio;
  • riga 7: indica che il campo [Weight] deve essere compreso nell'intervallo [1,200];
  • riga 9: indica che il campo [Età] è obbligatorio;
  • riga 7: indica che il campo [Età] deve rientrare nell'intervallo [1,150];

L'azione che utilizza questo modello sarà la seguente [Azione08]:


    // Action08
    public ContentResult Action08(ActionModel02 modèle)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("Contrôleur={0}, Action={1}, poids={2}, âge={3}, valide={4}, erreurs={5}", RouteData.Values["controller"], RouteData.Values["action"], modèle.Poids, modèle.Age, ModelState.IsValid, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

  • Riga 2: L'azione riceve un'istanza del modello [ActionModel02];

Eseguiamo alcuni test:

Image

Image

Image

Image

Gli errori vengono rilevati correttamente. Ora aggiorniamo il modello come segue:


using System.ComponentModel.DataAnnotations;
namespace Exemple_02.Models
{
  public class ActionModel02
  {
    [Required]
    [Range(1, 200)]
    public double Poids { get; set; }
    [Required]
    [Range(1, 150)]
    public int Age { get; set; }
  }
}

Righe 8 e 11: le proprietà non possono più avere il valore [null]. Compiliamo ed eseguiamo nuovamente il test senza parametri:

Image

L'assenza di parametri ha fatto sì che le proprietà [Weight] e [Age] mantenessero il valore acquisito al momento dell'istanziazione del modello: 0. A questo punto avviene la convalida. L'attributo [Required] viene quindi soddisfatto. Possiamo vedere che il messaggio di errore sopra riportato riguarda l'attributo [Range]. Pertanto, per verificare la presenza di un parametro, la proprietà associata deve essere nullable, ovvero deve poter accettare il valore null.

Torniamo al modello iniziale [ActionModel02] e consideriamo un'azione il cui modello consiste in un'istanza [ActionModel02] e un tipo [DateTime] nullable:


    // Action09
    public ContentResult Action09(ActionModel02 modèle, DateTime? date)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("Contrôleur={0}, Action={1}, poids={2}, âge={3}, date={4}, valide={5}, erreurs={6}", RouteData.Values["controller"], RouteData.Values["action"], modèle.Poids, modèle.Age, date, ModelState.IsValid, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

Eseguiamo alcuni test:

Image

Non abbiamo passato alcun parametro all'azione. Gli attributi [Required] sulle proprietà [Weight] e [Age] hanno funzionato correttamente. La data, tuttavia, ha ricevuto il valore null e non sono stati segnalati errori.

Ora passiamo dei parametri non validi:

Image

Ora passiamo valori validi:

Image

Esaminiamo altri vincoli di convalida. Il nuovo modello di azione è il seguente:

Image


using System.ComponentModel.DataAnnotations;
namespace Exemple_02.Models
{
  public class ActionModel03
  {
    [Required(ErrorMessage = "Le paramètre email est requis")]
    [EmailAddress(ErrorMessage = "Le paramètre email n'a pas un format valide")]
    public string Email { get; set; }
 
    [Required(ErrorMessage = "Le paramètre jour est requis")]
    [RegularExpression(@"^\d{1,2}$", ErrorMessage = "Le paramètre jour doit avoir 1 ou 2 chiffres")]
    public string Jour { get; set; }
 
    [Required(ErrorMessage = "Le paramètre info1 est requis")]
    [MaxLength(4, ErrorMessage = "Le paramètre info1 ne peut avoir plus de 4 caractères")]
    public string Info1 { get; set; }
 
    [Required(ErrorMessage = "Le paramètre info2 est requis")]
    [MinLength(2, ErrorMessage = "Le paramètre info2 ne peut avoir moins de 2 caractères")]
    public string Info2 { get; set; }
 
    [Required(ErrorMessage = "Le paramètre info3 est requis")]
    [MinLength(4, ErrorMessage = "Le paramètre info3 doit avoir 4 caractères exactement")]
    [MaxLength(4, ErrorMessage = "Le paramètre info3 doit avoir 4 caractères exactement")]
    public string Info3 { get; set; }
  }
}

  • riga 6: l'attributo [Required], questa volta con un messaggio di errore definito dall'utente;
  • riga 7: l'attributo [EMailAddress] richiede che il campo [Email] contenga un indirizzo e-mail valido;
  • riga 11: l'attributo [RegularExpression] richiede che il campo [Day] contenga una stringa di una o due cifre. Il primo parametro è l'espressione regolare rispetto alla quale il campo deve essere convalidato;
  • riga 15: l'attributo [MaxLength] richiede che il campo [Info1] non contenga più di 4 caratteri;
  • riga 19: l'attributo [MinLength] richiede che il campo [Info2] contenga almeno 2 caratteri;
  • righe 23-24: gli attributi combinati [MaxLength] e [MinLength] richiedono che il campo [Info3] contenga esattamente 4 caratteri;

L'azione [Action10] utilizzerà questo modello:


// Action10
    public ContentResult Action10(ActionModel03 modèle)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("email={0}, jour={1}, info1={2}, info2={3}, info3={4}, erreurs={5}",
        modèle.Email, modèle.Jour, modèle.Info1, modèle.Info2, modèle.Info3, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
    }

Eseguiamo alcuni test con questa azione.

Per prima cosa, senza parametri:

Image

Poi con parametri non validi:

Image

Poi con parametri validi:

Image

4.6. Modello di azione con vincoli di validità - 2

Introduciamo ulteriori vincoli di integrità. Il nuovo modello di azione sarà la seguente classe [ActionModel04]:


using System.ComponentModel.DataAnnotations;
 
namespace Exemple_02.Models
{
  public class ActionModel04
  {
    [Required(ErrorMessage="Le paramètre url est requis")]
    [Url(ErrorMessage="URL invalide")]
    public string Url { get; set; }
    [Required(ErrorMessage = "Le paramètre info1 est requis")]
    public string Info1 { get; set; }
    [Required(ErrorMessage = "Le paramètre info2 est requis")]
    [Compare("Info1",ErrorMessage="Les paramètres info1 et info2 doivent être identiques")]
    public string Info2 { get; set; }
    [Required(ErrorMessage = "Le paramètre cc est requis")]
    [CreditCard(ErrorMessage = "Le paramètre cc n'est pas un n° de carte de crédit valide")]
    public string Cc { get; set; }
  }
}
  • riga 8: richiede che il campo annotato sia un URL valido;
  • riga 13: richiede che le proprietà [Info1] e [Info2] abbiano lo stesso valore;
  • riga 16: richiede che il campo annotato sia un numero di carta di credito valido.

L'azione che utilizza questo modello sarà la seguente:


    // Action11
    public ContentResult Action11(ActionModel04 modèle)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("URL={0}, Info1={1}, Info2={2}, CC={3},erreurs={4}",
        modèle.Url, modèle.Info1, modèle.Info2, modèle.Cc, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

Per testare l'azione [Action11], utilizziamo l'applicazione [Advanced Rest Client]:

  • in [1], l'URL dell'azione [Action11];
  • in [2], questo URL verrà richiesto con un POST;
  • in [3], selezionare la scheda [Form];
  • in [4], i valori dei quattro parametri previsti. Questa inizializzazione è una funzionalità fornita da [ARC]. I parametri effettivamente inviati possono essere visualizzati nella scheda [Raw] [5];
  • in [6], i parametri POST.

Per questa richiesta, riceviamo la seguente risposta:

Image

Proviamo a passare parametri non validi:

Image

Otteniamo quindi la seguente risposta:

Image

4.7. Modello di azione con vincoli di validità - 3

A volte i vincoli di integrità disponibili non sono sufficienti. In tal caso, è possibile crearne di propri. In particolare, è possibile utilizzare un modello che implementi l'interfaccia [IValidatableObject]. In questo caso, si aggiungono le proprie validazioni del modello al metodo [Validate] di questa interfaccia. Vediamo un esempio. Il nuovo modello di azione sarà la seguente classe [ActionModel05]:


using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
 
namespace Exemple_02.Models
{
  public class ActionModel05 : IValidatableObject
  {
    [Required(ErrorMessage = "Le paramètre taux est requis")]
    public double? Taux { get; set; }
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
      List<ValidationResult> résultats = new List<ValidationResult>();
      bool ok = Taux < 4.2 || Taux > 6.7;
      if (!ok)
      {
        résultats.Add(new ValidationResult("Le paramètre taux doit être < 4.2 ou > 6.7", new string[] { "Taux" }));
      }
      return résultats;
    }
  }
}

  • riga 6: il modello implementa l'interfaccia [IValidatableObject];
  • riga 10: il metodo [Validate] di questa interfaccia. Restituisce una raccolta di elementi di tipo [ValidationResult]. Questo tipo incapsula gli errori da segnalare;
  • riga 9: un tasso valido è un tasso <4,2 o > 6,7;
  • riga 12: creiamo una lista vuota di elementi di tipo [ValidationResult];
  • riga 13: verifichiamo la validità della proprietà [Rate];
  • righe 14–17: se la proprietà [Rate] non è valida, viene aggiunto un elemento di tipo [ValidationResult] all'elenco dei risultati. Il primo parametro è un messaggio di errore. Il secondo parametro, che è facoltativo, è una raccolta delle proprietà interessate da questo errore.

L'azione che utilizza questo modello sarà la seguente:


    // Action12
    public ContentResult Action12(ActionModel05 modèle)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("taux={0}, erreurs={1}", modèle.Taux, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

Ecco un esempio di esecuzione:

Image

4.8. Modello di azione di tipo Tabella o Lista

Si consideri la seguente azione [Action13]:


// Action13
    public ContentResult Action13(string[] data)
    {
      string strData = "";
      if (data != null && data.Length != 0)
      {
        strData = string.Join(",", data);
      }
      string texte = string.Format("data=[{0}]", strData);
      return Content(texte, "text/plain", Encoding.UTF8);
    }

  • Riga 2: Il modello di azione è costituito da un array di [string]. Ci permette di recuperare un parametro denominato [data], che può comparire più volte nei parametri della richiesta, come ad esempio in [?data=data1&data=data2&data=data3]. I vari parametri [data] presenti nella richiesta popoleranno l'array [data] nel modello di azione. Questo scenario si verifica con gli elenchi a discesa. Il browser invia quindi i diversi valori selezionati dall'utente, tutti con lo stesso nome di parametro.

Ecco un esempio:

Image

Il modello può anche essere un elenco:


    // Action14
    public ContentResult Action14(List<int> data)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string strData = "";
      if (data != null && data.Count != 0)
      {
        strData = string.Join(",", data);
      }
      string texte = string.Format("data=[{0}], erreurs=[{1}]", strData, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

Il modello qui è un elenco di numeri interi (riga 2). Ecco il primo risultato:

Image

e una seconda:

Image

4.9. Filtraggio di un modello di azione

A volte disponiamo di un modello, ma desideriamo che solo determinati elementi del modello vengano inizializzati dalla richiesta HTTP. Consideriamo il seguente modello di azione [ActionModel06]:


using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
 
namespace Exemple_02.Models
{
  [Bind(Exclude = "Info2")]
  public class ActionModel06
  {
    [Required(ErrorMessage = "Le paramètre [info1] est requis")]
    public string Info1 { get; set; }
 
    public string Info2 { get; set; }
  }
}
  • righe 9-10: il parametro [info1] è obbligatorio;
  • riga 6: il parametro [info2] alla riga 12 è escluso dal binding della richiesta HTTP al suo modello.

L'azione sarà la seguente [Action15]:


    // Action15
    public ContentResult Action15(ActionModel06 modèle)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("valide={0}, info1={1}, info2={2}, erreurs={3}", ModelState.IsValid, modèle.Info1, modèle.Info2, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

Ecco un esempio di esecuzione:

  • in [1]: passiamo il parametro [info2] nell'URL;
  • in [2]: la proprietà [Info2] del modello di azione rimane vuota.

4.10. Estensione del modello di binding dei dati

Rivediamo l'architettura di esecuzione di un'azione:

La classe dell'azione viene istanziata all'inizio della richiesta del client e distrutta alla fine della stessa. Pertanto, non può essere utilizzata per memorizzare dati tra una richiesta e l'altra, anche se viene chiamata ripetutamente. Potresti voler memorizzare due tipi di dati:

  • dati condivisi da tutti gli utenti dell'applicazione web. Si tratta generalmente di dati di sola lettura. Per implementare questa condivisione dei dati vengono utilizzati tre file:
    • [Web.Config]: il file di configurazione dell'applicazione
    • [Global.asax, Global.asax.cs]: consentono di definire una classe, denominata classe dell'applicazione globale, la cui durata è quella dell'applicazione, nonché gestori per determinati eventi della stessa applicazione.

La classe di applicazione globale consente di definire dati che saranno disponibili per tutte le richieste provenienti da tutti gli utenti.

  • dati condivisi tra le richieste provenienti dallo stesso client. Questi dati sono memorizzati in un oggetto chiamato Sessione. Ci riferiamo a questo come alla sessione del client per indicare la memoria del client. Tutte le richieste provenienti da un client hanno accesso a questa sessione. Possono memorizzare e leggere informazioni al suo interno.

Sopra, mostriamo i tipi di memoria a cui un'azione ha accesso:

  • la memoria dell'applicazione, che contiene principalmente dati di sola lettura ed è accessibile a tutti gli utenti;
  • la memoria di un utente specifico, o sessione, che contiene dati in lettura/scrittura ed è accessibile alle richieste successive dello stesso utente;
  • non mostrata sopra, esiste una memoria di richiesta, o contesto di richiesta. La richiesta di un utente può essere elaborata da diverse azioni successive. Il contesto di richiesta consente all'Azione 1 di passare informazioni all'Azione 2.

Diamo un'occhiata a un primo esempio che illustra questi diversi tipi di memoria:

Per prima cosa, modifichiamo il file [Web.config] del progetto [Example-02] come segue:


  <appSettings>
    <add key="webpages:Version" value="2.0.0.0" />
    ...
    <add key="infoAppli1" value="infoAppli1"/>
</appSettings>

Aggiungiamo la riga 4, che associa il valore [infoAppli1] alla chiave [infoAppli1]. Questi saranno i nostri dati nell'ambito [Application]: saranno accessibili a tutte le richieste di tutti gli utenti.

Successivamente, modifichiamo il metodo [Application_Start] nel file [Global.asax]. Questo metodo viene eseguito una volta all'avvio dell'applicazione. È qui che dobbiamo utilizzare il file [Web.config]:


    protected void Application_Start()
    {
      AreaRegistration.RegisterAllAreas();
 
      WebApiConfig.Register(GlobalConfiguration.Configuration);
      FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
      RouteConfig.RegisterRoutes(RouteTable.Routes);
      BundleConfig.RegisterBundles(BundleTable.Bundles);
      // intialisation application
      Application["infoAppli1"] = ConfigurationManager.AppSettings["infoAppli1"];
}

Aggiungiamo la riga 10. Essa svolge due operazioni:

  • recupera il valore della chiave [infoAppli1] dal file [Web.config] utilizzando la classe [System.Configuration.ConfigurationManager];
  • lo memorizza nel dizionario [HttpApplication.Application], associato alla chiave [infoAppli1]. Tutte le azioni hanno accesso a questo dizionario.

Nello stesso file [Global.asax], aggiungiamo il seguente metodo [Session_Start]:


    protected void Session_Start()
    {
      // initialisation compteur
      Session["compteur"] = 0;
}

Il metodo [Session_Start] viene eseguito per ogni nuovo utente. Che cos'è un nuovo utente? Un utente viene "tracciato" da un token di sessione. Questo token è:

  • creato dal server web e inviato al nuovo utente nelle intestazioni HTTP della prima risposta inviata;
  • rinviato dal browser dell’utente ad ogni nuova richiesta che effettua. Ciò permette al server di riconoscere l’utente e di gestire uno spazio di memoria a lui dedicato chiamato sessione dell’utente.

Il server web riconosce di avere a che fare con un nuovo utente quando l'utente non invia un token di sessione. Il server ne crea quindi uno per lui.

Nella riga 4 sopra, inseriamo un contatore nella sessione dell'utente che verrà incrementato ad ogni richiesta proveniente da quell'utente. Questo illustra la memoria associata a un utente. La classe [Session] viene utilizzata come un dizionario (riga 4).

Fatto ciò, scriviamo la seguente azione [Action16]:


// Action16
    public ContentResult Action16()
    {
      // on récupère le contexte de la requête HTTP
      HttpContextBase contexte = ControllerContext.HttpContext;
      // on récupère les infos de portée Application
      string infoAppli1 = contexte.Application["infoAppli1"] as string;
      // et celles de portée Session
      int? compteur = contexte.Session["compteur"] as int?;
      compteur++;
      contexte.Session["compteur"] = compteur;
      // la réponse au client
      string texte = string.Format("infoAppli1={0}, compteur={1}", infoAppli1, compteur);
      return Content(texte, "text/plain", Encoding.UTF8);
}

  • riga 5: recuperiamo il contesto della richiesta HTTP attualmente in elaborazione. Questo contesto ci darà accesso ai dati negli ambiti [Application] e [Session];
  • riga 7: recuperiamo le informazioni dell'ambito [Application];
  • riga 9: recuperiamo il contatore dalla sessione;
  • righe 10–11: viene incrementato e poi memorizzato nuovamente nella sessione;
  • Righe 13–14: entrambe le informazioni vengono inviate al client.

Ecco alcuni esempi di esecuzione:

[Azione16] viene richiesta una volta [1], poi la pagina viene aggiornata [F5] due volte [2]:

In [2], il client ha effettuato un totale di tre richieste. Ogni volta, è stato in grado di recuperare il contatore aggiornato dalla richiesta precedente.

Per simulare un secondo utente, utilizziamo un secondo browser per richiedere lo stesso URL:

In [3], il secondo utente recupera con successo le stesse informazioni relative all'ambito [Application], ma dispone di un proprio contatore relativo all'ambito [Session].

Torniamo al codice dell'azione [Action16]:


// Action16
    public ContentResult Action16()
    {
      // on récupère le contexte de la requête HTTP
      HttpContextBase contexte = ControllerContext.HttpContext;
      // on récupère les infos de portée Application
      string infoAppli1 = contexte.Application["infoAppli1"] as string;
      // et celles de portée Session
      int? compteur = contexte.Session["compteur"] as int?;
      compteur++;
      contexte.Session["compteur"] = compteur;
      // la réponse au client
      string texte = string.Format("infoAppli1={0}, compteur={1}", infoAppli1, compteur);
      return Content(texte, "text/plain", Encoding.UTF8);
}

Uno degli obiettivi del framework ASP.NET MVC è rendere i controller e le azioni testabili in modo isolato senza un server web. Tuttavia, come si vede alla riga 5, il contesto della richiesta HTTP è necessario per recuperare i dati dagli ambiti [Application] e [Session]. Proponiamo di creare una nuova azione [Action17] che riceva i dati degli ambiti [Application] e [Session] come parametri:


    // Action17
    public ContentResult Action17(ApplicationModel applicationData, SessionModel sessionData)
    {
      // retrieve range info Application
      string infoAppli1 = applicationData.InfoAppli1;
      // and Session range
      int compteur = sessionData.Compteur++;
      // customer response
      string texte = string.Format("infoAppli1={0}, compteur={1}", infoAppli1, compteur);
      return Content(texte, "text/plain", Encoding.UTF8);
}

Il codice non ha più alcuna dipendenza dalla richiesta HTTP. Può quindi essere testato indipendentemente da un server web.

Vediamo come procedere. Innanzitutto, dobbiamo creare le classi [ApplicationModel] e [SessionModel], che incapsuleranno rispettivamente i dati dell'ambito [Application] e [Session]. Sono le seguenti:


namespace Exemple_02.Models
{
  public class ApplicationModel
  {
    public string InfoAppli1 { get; set; }
  }
}


namespace Exemple_02.Models
{
  public class SessionModel
  {
    public int Compteur { get; set; }
    public SessionModel()
    {
      Compteur = 0;
    }
  }
}

Successivamente, dobbiamo modificare i metodi [Application_Start] e [Session_Start] nel file [Global.asax]:


public class MvcApplication : System.Web.HttpApplication
  {
    protected void Application_Start()
    {
      AreaRegistration.RegisterAllAreas();
 
      WebApiConfig.Register(GlobalConfiguration.Configuration);
      FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
      RouteConfig.RegisterRoutes(RouteTable.Routes);
      BundleConfig.RegisterBundles(BundleTable.Bundles);
      // intialisation application - case 1
      Application["infoAppli1"] = ConfigurationManager.AppSettings["infoAppli1"];
      // intialisation application - case 2
      ApplicationModel data=new ApplicationModel();
      data.InfoAppli1=ConfigurationManager.AppSettings["infoAppli1"];
      Application["data"] = data;
    }
 
    protected void Session_Start()
    {
      // counter initialization - case 1
      Session["compteur"] = 0;
      // counter initialization - case 2
      Session["data"] = new SessionModel();
    }
  }

  • riga 14: viene creata un'istanza di [ApplicationModel];
  • riga 15: viene inizializzata;
  • riga 16: e inserita nel dizionario [Application], associata alla chiave [data]. [Application] è una proprietà della classe [HttpApplication] della riga 1;
  • riga 24: viene creata un'istanza di [SessionModel] e inserita nel dizionario [Session], associata alla chiave [data]. [Session] è una proprietà della classe [HttpApplication] della riga 1;

Sulla base di quanto visto finora, la firma


    public ContentResult Action17(ApplicationModel applicationData, SessionModel sessionData)

indica che la richiesta HTTP elaborata dall'azione deve includere parametri denominati [applicationData] e [sessionData]. Ma non sarà così. Dobbiamo creare un nuovo modello di associazione dati in modo che, quando un'azione riceve un tipo come parametro:

  • [ApplicationModel], le vengano forniti i dati con ambito [Application] e chiave [data];
  • [SessionModel], le vengono forniti i dati con ambito [Session] e chiave [data].

Per farlo, dobbiamo creare classi che implementino l'interfaccia [IModelBinder].

Iniziamo creando una cartella [Infrastructure] nel progetto [Example-02]:

Image

Al suo interno, creiamo la seguente classe [ApplicationModelBinder]:


using System.Web.Mvc;
 
namespace Exemple_02.Infrastructure
{
  public class ApplicationModelBinder : IModelBinder
  {
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
      // render scope data [Application]
      return controllerContext.RequestContext.HttpContext.Application["data"];
    }
  }
}

  • riga 5: la classe implementa l'interfaccia [IModelBinder]. Per comprenderne il codice, è necessario sapere che verrà chiamata ogni volta che un'azione avrà un parametro di tipo [ApplicationModel]. Questo collegamento [ApplicationModel] --> [ApplicationModelBinder] verrà stabilito all'avvio dell'applicazione, nel metodo [Application_Start] di [Global.asax];
  • riga 7: l'unico metodo dell'interfaccia [IModelBinder];
  • Riga 7: il parametro [ControllerContext] ci consente di accedere alla richiesta HTTP attualmente in elaborazione;
  • riga 7: il parametro di tipo [ModelBindingContext] ci dà accesso alle informazioni sul modello da costruire, in questo caso il tipo [ApplicationModel];
  • riga 7: il risultato di [BindModel] è l'oggetto che verrà assegnato al parametro associato, in questo caso un parametro di tipo [ApplicationModel];
  • riga 10: restituiamo semplicemente l'oggetto con ambito [Application] e chiave [data].

La classe [ SessionModelBinder] segue lo stesso schema:


using System.Web.Mvc;
 
namespace Exemple_02.Infrastructure
{
  public class SessionModelBinder : IModelBinder
  {
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
      // render scope data [Session]
      return controllerContext.HttpContext.Session["data"];
    }
  }
}

Non resta che associare ciascun modello [XModel] al proprio [XModelBinder]. Ciò avviene nel metodo [Application_Start] di [Global.asax]:


    protected void Application_Start()
    {
....
      // intialisation application - case 2
      ApplicationModel data=new ApplicationModel();
      data.InfoAppli1=ConfigurationManager.AppSettings["infoAppli1"];
      Application["data"] = data;
      // model binders
      ModelBinders.Binders.Add(typeof(ApplicationModel), new ApplicationModelBinder());
      ModelBinders.Binders.Add(typeof(SessionModel), new SessionModelBinder());
}

  • riga 9: quando un'azione ha un parametro di tipo [ApplicationModel], verrà chiamato il metodo [ApplicationModelBinder.Bind]. Sappiamo che restituisce i dati nell'ambito [Application] associati alla chiave [data];
  • riga 10: lo stesso vale per il tipo [SessionModel].

Torniamo alla nostra azione [Action17]:


    // Action17
    public ContentResult Action17(ApplicationModel applicationData, SessionModel sessionData)
    {
      // retrieve range info Application
      string infoAppli1 = applicationData.InfoAppli1;
      // and Session range
      sessionData.Compteur++;
      int compteur = sessionData.Compteur;
      // customer response
      string texte = string.Format("infoAppli1={0}, compteur={1}", infoAppli1, compteur);
      return Content(texte, "text/plain", Encoding.UTF8);
}

  • Riga 2: Quando viene chiamata [Action17], riceverà
    • primo parametro: i dati dell'ambito [Application] associati alla chiave [data],
    • il secondo parametro: i dati dell'ambito [Session] associati alla chiave [data];

Questi due insiemi di dati possono essere complessi quanto si desidera e possono includere, per uno, tutti i dati dell'ambito [Application] e, per l'altro, tutti i dati dell'ambito [Session].

Ecco un esempio dell'esecuzione dell'azione [Action17]:

Image

4.11. Legame tardivo del modello di azione

Abbiamo scritto la seguente [Action12]:


// Action12
    public ContentResult Action12(ActionModel05 modèle)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("taux={0}, erreurs={1}", modèle.Taux, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

Dietro le quinte, ASP.NET MVC:

  • crea un'istanza di tipo [ActionModel05] utilizzando il suo costruttore senza parametri;
  • la inizializza con le informazioni della richiesta che hanno lo stesso nome (senza distinzione tra maiuscole e minuscole) di una delle proprietà di [ActionModel05].

A volte questo comportamento non è quello che vogliamo. Ciò vale in particolare quando vogliamo utilizzare un costruttore specifico del modello di azione. Possiamo quindi procedere come segue:


    // Action18
    public ContentResult Action18()
    {
      ActionModel05 modèle = new ActionModel05();
      TryUpdateModel(modèle);
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("taux={0}, erreurs={1}", modèle.Taux, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

  • Riga 2: L'azione non riceve più parametri. Pertanto, non c'è più alcun binding automatico dei dati;
  • riga 4: creiamo noi stessi un'istanza del modello dell'azione. Qui potremmo utilizzare un costruttore diverso;
  • riga 5: inizializziamo il modello con le informazioni della richiesta. ASP.NET MVC esegue questa operazione. Lo fa nello stesso modo in cui lo avrebbe fatto se il modello fosse stato un parametro;
  • riga 6: ora ci troviamo nella stessa situazione dell'azione [Action12].

Ecco un esempio di esecuzione:

Image

4.12. Conclusione

Torniamo all'architettura di un'applicazione ASP.NET MVC:

Una richiesta [1] trasporta varie informazioni che ASP.NET MVC presenta [2a] all'azione sotto forma di un modello, che abbiamo chiamato modello di azione.

  • La richiesta HTTP del client arriva a [1];
  • in [2], le informazioni contenute nella richiesta vengono trasformate in un modello di azione [3];
  • In [4], l'azione, basandosi su questo modello, genererà una risposta. Questa risposta avrà due componenti: una vista V [6] e il modello M per quella vista [5];
  • la vista V [6] utilizzerà il proprio modello M [5] per generare la risposta HTTP destinata al client.

Nel modello MVC, l'azione [4] fa parte del C (controller), il modello di vista [5] è l'M e la vista [6] è la V.

Questo capitolo ha esaminato i meccanismi che collegano le informazioni trasportate dalla richiesta — che sono intrinsecamente stringhe — al modello dell’azione, che può essere una classe con proprietà di vario tipo. Abbiamo anche visto che è possibile convalidare il modello presentato all’azione. Infine, abbiamo visto come estendere questo modello per includere dati provenienti dagli ambiti [Session] e [Application].

Ci concentreremo ora sulla parte finale della catena di elaborazione della richiesta [1]: la creazione della vista [6] e del suo modello [5]. Questi due elementi sono generati dall'azione [4].