ASPNET Core MVC - Inf. Broccolucci
ASPNET Core MVC - Inf. Broccolucci
Anno 2017/2018
www.miosito.it
Richiesta (HTTP) di pagina HTML
http://www.miosito.it/home.html
(via stream TCP)
PHP
engine
La pagina viene processata da un software (PHP engine) in grado di eseguire il codice PHP. Il
risultato finale viene inviato al server, che lo ritrasmette al client (il quale riceve dunque del codice
HTML).
1.2 MVC
Il design pattern Model View Controller stabilisce la separazione di un’applicazione in tre tipi di
componenti; il pattern trova il suo naturale impiego nelle applicazioni web.
Model
us
a
us
a
Controller usa View
MVC viene incontro all’esigenza di separare la UI dalla gestione dei dati e dalla logica applicativa.
In questo schema:
(Le applicazioni realistiche prevedono anche dei componenti di business logic, servizi e accesso
dati.)
La figura in alto mostra i legami tra model, view e controller. Il model è indipendente da view e
controller: non deve dipendere dal tipo applicazione, architettura e UI. Lo stesso model dovrebbe
poter essere utilizzato in applicazioni desktop, mobile, web.
Il view dipende dal model, poiché ha la funzione di visualizzarlo, ma è indipendente dal controller,
e dunque non ha alcun riferimento ad esso.
Infine, il controller dipende sia da view che da model: ottiene il model da classi repository, context o
business e lo passa alle view .
1 Se è stata installata la funzionalità Application Insights, conviene disabilitarla; ciò riduce le dimensioni del
progetto e ne aumenta le performance.
2 Se sono state installate le funzionalità di Azure, è opportuno disabilitare la voce Host in the cloud. Per
semplicità, conviene inoltre disabilitare l’uso del protocollo HTTPS.
Di seguito mi focalizzerò sui file HomeController, Index e _Layout, i quali rappresentano l’ossa-
tura di una applicazione MVC.
Layout page
View
Di seguito mostro la view Index (home page del sito), e la layout page _Layout, che definisce la
struttura comune del sito. (Ho ridotto entrambe all’essenziale):
Index.cshtml _Layout.cshtml
La chiamata al metodo RenderBody() colloca la view all’interno della layout page. All’accesso al
sito (Index viene caricata automaticamente), si ottiene il seguente risultato:
<hr />
Footer (layout page)
</body>
</html>
@{Layout = "_Layout";}
Index.cshtml _Layout.cshtml
Come vedremo più avanti, ViewData può essere impiegato anche per passare dati dai controller
alle view.
/[<controller>=Home]/[<action>=Index][?<nome>=<valore>&...]
A titolo di esempio modifico la view Index, collocandovi un hyperlink che richiama la view About:
Sarà ASP.NET a elaborare i due tag e produrre un hyperlink che rispetta la sintassi HTML.
Nota bene: la direttiva addTagHelper rende disponibili i tag helpers per tutte le view.
2.7 Razor
Razor identifica la tecnologia che consente di utilizzare C# nelle view. Il link seguente contiene un
tutorial che ne spiega le basi:
https://docs.microsoft.com/en-us/aspnet/core/mvc/views/razor
4. About() esegue il metodo View() , il quale cerca una view di nome About nella cartella
“Views/Home”.
5. Prima di caricare la view, vengono eseguite le istruzioni contenute in _ViewImports e
_ViewStart, le quali dichiarano la layout page da utilizzare, importano i tag helper e
dichiarano i namespace.
6. La view About viene caricata e processata; il risultato viene integrato nella layout page
_Layout, precisamente nella posizione specificata da RenderBody() .
7. Anche la layout page viene processata; il risultato finale è puro HTML (più javascript e
CSS, se definiti), che viene inviato al client.
MotoGPRepository
Moto Pilota
La pagina referenzia due elementi statici, il foglio di stile e l’immagine del banner. Entrambi sono
memorizzati nella cartella wwwroot, all’interno della quale sono presenti anche le foto dei piloti.
Index Index()
@{ViewData["Title"] = "Home";} public IActionResult Index()
{
<div class="textcenter"> return View();
<img src="~/images/Homefoto.png"/> }
</div>
Honda
Yamaha Honda
Yamaha ElencoMoto()
Ducati Ducati
...
Ogni link referenzia l’action ElencoPilotiMoto() e specifica l’id della moto visualizzata. Ma poiché
la view riceve i dati dal controller, deve innanzitutto dichiarare il tipo del model, rappresentato da
una sequenza di moto.
@{ViewData["Title"] = "Elenco moto";}
All’interno della view, la sequenza di moto è accessibile mediante la parola chiave Model . Occorre
semplicemente “scorrerla” e visualizzare ogni moto mediante un hyperlink:
@{ViewData["Title"] = "Elenco moto";}
Nota bene: nell’hyperlink, per specificare l’id si può usare il tag helper asp-route-… , al quale sarà
assegnato il campo MotoId della moto. Alternativamente si può scrivere:
<a href="/Home/ElencoPilotiMoto/@m.MotoId">@m.Nome</a>
@model IEnumerable<Pilota>
<div>
Nota bene: il nome del pilota viene visualizzato mediante un hyperlink che l’utente può cliccare
per accedere alla pagina di informazioni sul pilota.
Interessante è il metodo ElencoPilotiMoto() ; questo viene chiamato quando l’utente, nella view
ElencoMoto, clicca su uno dei link generati da:
Nell’invocare il metodo, ASP.NET esegue il cosiddetto “binding” tra il parametro dell’URL, e cioè
MotoId , e il parametro del metodo ElencoPilotiMoto() .
Ma, in questo scenario particolare, esiste un bug nell’implementazione dei tag helper che obbliga
a impiegare un workaround semplice da implementare, ma meno semplice da comprendere.
Pertanto ho preferito definire due metodi separati per il caricamento della view ElencoPiloti.
Il metodo action InfoPilota() riceve l’id, carica il pilota corrispondente e lo passa alla view
omonima:
public class HomeController : Controller
{
MotoGPRepository repo = new MotoGPRepository();
...
public IActionResult InfoPilota(int id)
{
return View(repo.GetPilota(id));
}
}
<tr>
<th colspan="2"><h1>@Model.Nominativo</h1></th>
</tr>
<tr>
<th colspan="2" class="textcenter">
<img src="~/FotoPiloti/@Model.FileFoto" />
</th>
</tr>
<tr>
<td class="textcenter"><h3>@moto</h3></td>
</tr>
<tr>
<td class="textcenter">@statistiche</td>
</tr>
</table>
In questa view c’è una novità rispetto alle precedenti: in un blocco C# vengono impostate due
variabili stringa, utilizzate successivamente nel codice HTML. Ciò mostra una caratteristica di
razor: le variabili definite fuori dai metodi sono considerate globali e dunque accessibili ovunque
nella pagina.
• Il browser chiede la pagina contenente il form. Il server risponde con un form vuoto.
• Il browser invia al server i dati inseriti dall’utente (invio del form). Il server processa i dati, li
verifica e, dopo l’inserimento, reindirizza il browser a una nuova pagina.
CLIENT SERVER
Richiesta pagina HTML (form)
[verbo http: GET]
Il form è gestito mediante due metodi action. Il primo metodo carica la view contenente il form; il
secondo riceve i dati inseriti nel form e procede all’inserimento. Segue il primo dei due metodi:
public class HomeController : Controller
{
MotoGPRepository repo = new MotoGPRepository();
[HttpGet]
public IActionResult NuovoPilota()
{
return View();
}
}
[HttpPost]
public IActionResult NuovoPilota(Pilota pilota)
{
repo.NuovoPilota(pilota);
return RedirectToAction("ElencoPiloti");
}
• Il metodo è decorato con l’attributo [HttpPost] ; questo lo identifica come un metodo che
riceve i dati inseriti nel form.
• Sulla base del model dichiarato nella view, ASP.NET è in grado di “bindare” i dati ricevuti
alle corrispondenti proprietà del parametro pilota .
• Dopo aver inserito il pilota nel repository, il metodo reindirizza l’utente alla pagina elenco
piloti utilizzando RedirectToAction() , il quale esegue il metodo action corrispondente alla
view specificata.
(Nota bene: nell’attuale versione non viene presa in considerazione l’eventualità di errori, nei dati
come nel processo di inserimento.)
[HttpGet]
public IActionResult NuovoPilota()
{
ViewData["listaMoto"] = repo.GetElencoMoto();
return View();
}
[HttpPost]
public IActionResult NuovoPilota(Pilota pilota)
{
... // resta invariato
}
}
Nella view si memorizza innanzitutto l’elenco in una variabile; successivamente si usa il tag helper
asp-items per generare il tag select:
@{
Alla view NuovoPilota occorre aggiungere il tag input per la selezione del file; inoltre, perché sia
possibile l’upload, occorre aggiungere l’attributo enctype al form:
@{
ViewData["Title"] = "Nuovo pilota";
var listaMoto = ViewData["listaMoto"] as IEnumerable<Moto>;
}
@model Pilota
<div>
<form asp-controller="Home" enctype="multipart/form-data" ...>
<table class="center">
...
Nota bene: al tag input deve essere dato un nome ben definito, poiché dovrà essere lo stesso
utilizzato nel secondo parametro del metodo NuovoPilota() :
[HttpGet]
public IActionResult NuovoPilota()
{
ViewData["listaMoto"] = repo.GetElencoMoto();
return View();
}
[HttpPost]
public IActionResult NuovoPilota(Pilota p, IFormFile file)
{
//... crea pilota e salva file
}
}
Il tipo IFormFile memorizza le informazioni relative al file caricato e consente di salvarlo su disco.
pilota.FileFoto = Path.GetFileName(filePath);
}
pilota.Moto = repo.GetMoto(pilota.MotoId);
repo.NuovoPilota(pilota);
return RedirectToAction("ElencoPiloti");
}
}
using System.ComponentModel.DataAnnotations;
[Required]
public string Nome { get; set; }
[Required]
public string Cognome { get; set; }
[Range(1, 99)]
public int Numero { get; set; }
[Range(0, 450)]
public int Punti { get; set; }
[Range(0, 18)]
public int Vittorie { get; set; }
Gli attributi stabiliscono i criteri utilizzati per stabilire la validità dei dati inseriti. (Esistono altri tipi
di attributi, che consentono un elevato livello di personalizzazione nella validazione dei campi.)
Nel metodo action che elabora il form, prima di procedere all’elaborazione dell’input, occorre
verificare che il model sia valido:
return RedirectToAction("ElencoPiloti");
}
Entrambi i tag helper, insieme agli attributi e all’oggetto ModelState , consentono un elevato livello
di personalizzazione del processo di validazione. L’approccio standard è quello di utilizzare dei
tag span, adiacenti ai campi di input, per mostrare i singoli errori, e un tag div che riepiloghi gli
errori e/o visualizzi messaggi che riguardano la validità del modello in generale.
@{
ViewData["Title"] = "Nuovo pilota";
var listaMoto = ViewData["listaMoto"] as IEnumerable<Moto>;
}
@model Pilota
<div>
<form asp-controller="Home" enctype="multipart/form-data" ...>
<table class="center">
<tr>
<td colspan="2">
<div asp-validation-summary="ModelOnly" class="error-text"></div>
</td>
</tr>
<tr>
<td><label asp-for="Nome"></label></td>
<td><input asp-for="Nome"/>
<span asp-validation-for="Nome" class="error-text"></span>
</td>
</tr>
<tr>
<td><label asp-for="Cognome"></label></td>
<td><input asp-for="Cognome"/>
Il div posto all’inizio ha la funzione di riepilogo. Il valore ModelOnly dell’attributo indica che non
saranno visualizzati i singoli errori relativi ai campi. Questi vengono visualizzati attraverso dei tag
span, posizionati accanto ai tag di input. Alternativamente, si può decidere di usare soltanto il div
di riepilogo, specificando il valore All per l’attributo, in modo che vengano visualizzati automati-
camente tutti gli errori.
Nota bene: il primo parametro del metodo AddModelError() è vuoto; ciò contraddistingue un
errore di riepilogo ad uno che riguarda uno specifico campo.
[Required(ErrorMessage="*")]
[Range(1, 99, ErrorMessage ="Il numero deve essere compreso tra 1 e 99")]
public int Numero { get; set; }
...
}
Segue uno screen shot che mostra il risultato del processo di validazione. Il form è stato inviato
senza aver inserito il nome e con un numero della moto non valido:
• Non implementa alcune funzionalità, come ad esempio il lazy loading.3 (Non ha senso
dichiarare virtuali le reference property e le collection property.)
• Implementa un sistema di configurazione dell’entity model leggermente diverso.
• Implementa un diverso meccanismo per gestire la stringa di connessione (non è in grado
di ottenerla dai file App.Config e Web.Config).
• Ha un’architettura modulare, basata sul concetto di provider, che consente selezionare il
modulo necessario per dialogare con un determinato DBMS.
3 Questa funzionalità è in fase di implementazione e potrebbe essere rilasciata nella prossima versione.
[Table("Piloti")]
public class Pilota
{
public int PilotaId { get; set; }
...
public byte[] Foto { get; set; }
• il context viene dichiarato e creato globalmente, così può essere utilizzato in tutti i metodi
del controller.
• Mediante il metodo Include() , per ogni pilota viene inclusa la moto corrispondente. Si
tratta della tecnica di eager loading (che esiste anche in EF 6), ed è necessaria, poiché in EF
Core non si applica il lazy loading, senza considerare nell’ambito delle applicazioni web, il
lazy loading è normalmente inutilizzabile.
4 Qui non discuto sull’opportunità di memorizzare l’immagine nel database e nella stessa Piloti. In realtà si
tratta di una scelta errata, che diminuisce le performance.
@model IEnumerable<PilotaInfo>
<div>
<tr>
<th colspan="2" class="textcenter"><h1>@Model.Nominativo</h1></th>
</tr>
<tr>
<th colspan="2" class="textcenter">
<img src="~/FotoPiloti/@Model.FileFoto" />
<img src="data:image;base64,@System.Convert.ToBase64String(Model.Foto)" />
</th>
...
</table>
I byte dell’immagine vengono trasferiti insieme alla pagina e codificati in base64. Questa tecnica,
di per sé, non gestisce il caso in cui la proprietà Foto sia null . Una soluzione consiste nel verifica-
re questa condizione:
@{ViewData["Title"] = "Pilota";}
...
<table class="content">
Nota bene: viene restituito un file incorporato in un oggetto di tipo FileContentResult ; per farlo
uso il metodo File() .
Il metodo action viene richiamato direttamente dal tag img, specificando l’URL opportuno e
passando l’id del pilota:
@{ViewData["Title"] = "Pilota";}
...
<table class="content">
<tr>
<th colspan="2" class="textcenter"><h1>@Model.Nominativo</h1></th>
</tr>
<tr>
<th colspan="2" class="textcenter">
<img src="/Home/GetFotoPilota/@Model.PilotaId" />
</th>
...
</table>
services.AddMvc();
}
L’istruzione evidenziata ottiene un oggetto context, sulla base della configurazione effettuata nel
metodo ConfigureServices() :
L’ultima istruzione definisce la route predefinita utilizzata da MVC per stabilire i controller e i
metodi action da eseguire in risposta alle richieste dell’utente. (2.5)
5 Questo vale per il progetto “empty”. Il progetto “web application” prevede appunto di aggiungere il servizio
che implementa il pattern MVC.
services.AddMvc();
}
Perché il context possa utilizzare questa modalità di creazione, è necessario che definisca un
costruttore appropriato, in grado di ricevere dall’esterno le opzioni di configurazione:
public class MotoGPContext: DbContext
{
public MotoGPContext(DbContextOptions options):base(options) {}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
...
}
public DbSet<Pilota> Piloti { get; set; }
public DbSet<Moto> Moto { get; set; }
}
Infine, nei controller è necessario aggiungere un parametro al costruttore: sarà ASP.NET, quando
crea il controller, a costruire il context e a passarlo come argomento:
public class HomeController : Controller
{
IHostingEnvironment hostEnv;
MotoGPContext db = new MotoGPContext();
MotoGPContext db;
"Logging": {
"IncludeScopes": false,
"LogLevel": {
services.AddMvc();
}
7.4.2 Conclusioni
Questo approccio ha il vantaggio di centralizzare il codice di creazione e configurazione del
context. Semplicemente modificando Startup , e senza intervenire nella classe context, è possibile
cambiare la configurazione utilizzata, compresa l’origine del database.
ASP.NET Core fornisce tutti i servizi necessari al processo di autenticazione; di seguito introduco
le basi minime per implementare i processi di login, logout, e per conoscere lo stato dell’utente:
anonimo/autenticato.
LOGIN
La home consente agli utenti anonimi di autenticarsi e a quelli già autenticati di eseguire il logout.
login è un form HTML che chiede le credenziali dell’utente. L’invio del form produce l’esecuzione
del metodo Login() dell’home controller.
8.1.1 Login
Come ogni form, anche quello di login è gestito mediante due metodi; il primo che carica il form,
il secondo che ne elabora i dati:
using Microsoft.AspNetCore.Authentication.Cookies;
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
...
6 ASP.NET Core fornisce anche un’infrastruttura per la registrazione e la memorizzazione del profilo utente.
HttpContext.SignInAsync(scheme, principal).Wait();
}
Il metodo SignIn() :
8.1.2 Logout
public IActionResult Logout() // chiamato dal link Logout della home page
{
string scheme = CookieAuthenticationDefaults.AuthenticationScheme;
HttpContext.SignOutAsync(scheme).Wait();
return RedirectToAction("Index");
}
}
Dopo l’esecuzione di SignOutAsync() , le successive richieste dell’utente non sono più associate
all’identità precedentemente creata: l’utente è ritornato ad essere anonimo.
HttpContext
HttpContext è una proprietà dell’home controller che fornisce l’accesso a tutte le
informazioni e i servizi relativi al contesto della richiesta in corso e, in generale, della
sessione dell’utente. Come vedremo, questo oggetto è accessibile (sotto altro nome)
anche nelle view.
@{
var identity = Context.User.Identity;
}
<div>
@if (identity.IsAuthenticated )
{
<span>@identity.Name | </span>
<a asp-controller="Home" asp-action="Logout">Logout</a>
}
else
{
<a asp-controller="Home" asp-action="Login">Login</a>
<a asp-controller="Home" asp-action="Register">Register</a>
}
</div>
Alcune considerazioni:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
Nota bene: il metodo AddCookie() esiste in più versioni, e consente di personalizzare il servizio di
autenticazione.