Lezione 10 – Le View Components di .Net 6
Abbiamo già incontrato le Partial View e le View Components per qualche aspetto sono simili, ma differiscono per il contenuto dei dati utilizzati. Nelle Partial View possiamo utilizzare solo un modello già associato alla View chiamante, le View Components utilizzano i dati definiti nel codice di chiamata indipendente dal modello associato alla View. Per chiarire, quando chiamo una ViewComponent quello che viene visualizzato sarà quello che in fase di controllo viene definito e poi composto nella vista corrispondente.
Forse è meglio fare un esempio. Nella nostra HomePage al momento non ci sono molte informazioni per un utente che entrasse nel sito. Potremmo aggiungere un blocco di visualizzazione dei prodotti in promozione. Ora possiamo associare un Modello della tabella Product alla HomePage e fase di esecuzione del codice dell’Action “Index” inserire una query per selezionare i prodotti in promozione e poi visualizzarle con delle schede prodotto nel View. Tutto questo però varrebbe solo per quella pagina, se volessimo riutilizzare lo stesso approccio in un’altra pagina dovremo duplicare il codice e saremo comunque obbligati ad avere un’associazione al Modello. Se invece utilizziamo una ViewComponent sia il lavoro di selezione dei dati che la visualizzazione viene demandato alla componente di visualizzazione. Che poi potrà essere utilizzata n volte in altre pagine.
.Net 6 e il Compiler di Visual Studio vuole le View Components in un posto o percorso specifico “Views/Shared/Components”. In realtà ci sono altri due percorsi utilizzabili, poi forse li prenderemo in considerazione, per il momento semplifichiamoci la vita e utilizziamo questo, anche perché è quello consigliato da Microsoft.
Creiamo un folder “Components” in “Views/Shared” e poi un folder “PromoProduct” in “Views/Shared/Components”.
Iniziamo con del codice base per testare la componente di visualizzazione. Creiamo una classe “PromoProductViewComponent.cs” che fungerà da controllo.
using FoodShop.Data;
using FoodShop.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace FoodShop.Views.Shared.Components.PromoProduct
{
public class PromoProductViewComponent : ViewComponent
{
private readonly ApplicationDbContext _context;
public PromoProductViewComponent(ApplicationDbContext context)
{
_context = context;
}
public async Task<IViewComponentResult> InvokeAsync()
{
var items = await _context.Products.Where(p => p.PromoDiscount > 0).ToListAsync(); ;
return View(items);
}
}
}
E la View “Default.cshtml”. Il nome della vista non è casuale, ma richiesto.
@model IEnumerable<Product>
<table class="table table-striped text-start">
<thead>
<tr>
<th>
Codice
</th>
<th>
@Html.DisplayNameFor(model => model.Name)
</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Id)
</td>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
</tr>
}
</tbody>
</table>
Adesso per richiamare la nostra ViewComponent dalla HomePage modifichiamo “Views/Home/Index.cshtml” come segue.
… <h1 class="display-4">Lezioni Food Shop</h1> <div class="row ps-5 pe-5"> <div class="col-sm-12 col-md-9 border"><div class"pt-2 pb-2"> @await Component.InvokeAsync("PromoProduct") </div></div> <div class="col-sm-12 col-md-3 border"> <h2>Area Banner</h2> <br /> <br /> </div> </div> …
Possiamo notare che sia nella vista Index.cshtml che nella classe PromoProductViewComponent si fa largo uso dell’esecuzione asincrona. Questo perché per tutte le operazioni che richiedono un accesso al database non possiamo avere la certezza di una risposta immediata, quindi è più prudente ed efficiente attendere che i dati sia tornati indietro prima di visualizzarli.
Lanciamo il Debug, la visualizzazione dovrebbe essere più o meno questa.
Vedremo dopo come migliorare la visualizzazione dei prodotti in promozione, passiamo all’aspetto generale della HomePage e ai Banner. Creiamo un nuovo ViewComponent per visualizzarli.
Nel folder Components creare un nuovo folder “HpBanner”, poi la classe “HpBannerViewComponent.cs”
using FoodShop.Data;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace FoodShop.Views.Shared.Components.HpBanner
{
public class HpBannerViewComponent : ViewComponent
{
private readonly ApplicationDbContext _context;
public HpBannerViewComponent(ApplicationDbContext context)
{
_context = context;
}
public async Task<IViewComponentResult> InvokeAsync()
{
var items = await _context.Banners.Where(b => b.IdBanner == "homepagehead").ToListAsync(); ;
return View(items);
}
}
}
E nella vista “Default.cshtml” quello che segue.
@model IEnumerable
@foreach (var item in Model)
{
<div class="mb-4">
<a href="@Html.DisplayFor(modelItem => item.URL)" class="text-decoration-none" target="_blank" rel="noopener noreferrer">
<img src="~/images/banners/@item.IdFile" class="img-fluid w-100" alt="@item.IdFile" title="@item.Titolo">
<h5>@item.Titolo</h5>
</a>
</div>
}
Modifichiamo la HomePage per aggiungere i banner e togliamo il bordo che serviva solo a descrivere la due aree Prodotti e Banner.
… <h1 class="display-4">Lezioni Food Shop</h1> <div class="row ps-5 pe-5"><div class="col-sm-12 col-md-10"> <h2>Prodotti in Promozione</h2> <div class"pt-2 pb-2"> @await Component.InvokeAsync("PromoProduct") </div> </div> <div class="col-sm-12 col-md-2"> <div class"pt-2 pb-2"> @await Component.InvokeAsync("HpBanner") </div> </div></div> …
Adesso andiamo a migliorare la visualizzazione dei prodotti in promozione. Creiamo una Partial View “_ProductCard.cshtml” nel folder Shared.
@model Product
<div class="card border-0 shadow mb-2">
<img src="~/images/products/@Model.Image" class="card-img-top" alt="@Model.Image">
<div class="card-body">
<h4>@Model.Name</h4>
<p class="card-text">
<span class="text-danger">
@for (var n = 1; n <= Model.Quote; ++n)
{
<i class="bi-star-fill" style="font-size: 1.3rem;"></i>
}
@for (var n = Model.Quote; n < 5; ++n)
{
<i class="bi-star" style="font-size: 1.3rem;"></i>
}
</span>
@if (Model.InStock > 0)
{
<span>@ViewBag.InStock: <b class="text-success">Immediata</b></span>
}
else
{
if (Model.InOrder > 0)
{
<span>@ViewBag.InStock: <b class="text-warning">In Ordine</b></span>
}
else
{
<span>@ViewBag.InStock: <b class="text-danger">Non Disponibile</b></span>
}
}
<br />
@if (Model.PromoDiscount > 0)
{
<span>@Model.Price.Value:</span>
<span class="text-decoration-line-through">@Model.Price.Value</span>
<span>(-@Html.DisplayFor(modelItem => Model.PromoDiscount)%) <b>@(decimal.Round((Model.Price - ((Model.Price * Model.PromoDiscount) / 100)).Value, 2))</b></span>
}
else
{
<span>@ViewBag.Price: <b>@Model.Price.Value.ToString()</b></span>
}
<div>
<button class="btn btn-success"><i class="bi-cart-plus" style="font-size: 1.3rem;"></i></button>
<button class="btn btn-light"><i class="bi-heart" style="font-size: 1.3rem;"></i></button>
</div>
</p>
</div>
</div>
E sostituiamo il contenuto di Default.cshtml della ViewComponentPromoProduct con quello che segue.
@model IEnumerable<Product>
<h2 class="bg-secondary text-light">Prodotti in Promozione</h2>
<div class="row" data-masonry='{"percentPosition": true }'>
@foreach (var item in Model)
{
<div class="col-lg-4 col-md-6 col-12">
<partial name="_ProductCard" model="item" />
</div>
}
</div>
Modifichiamo il contenuto di “/Views/Home/Index.cshtml” con quello che segue.
@{
ViewData["Title"] = "Lezioni Food Shop";
}
<div class="text-center">
<div id="carouselHPFade" class="carousel slide carousel-fade" data-bs-ride="carousel">
<div class="carousel-inner">
@for (var i = 1; i <= 5; i++)
{
if (i == 1)
{
<div class="carousel-item active">
<img src="~/images/homepage/@string.Format("hp0{0}.jpg", i)" class="d-block w-100" alt="img n.@i.ToString()" />
</div>
}
else
{
<div class="carousel-item">
<img src="~/images/homepage/@string.Format("hp0{0}.jpg", i)" class="d-block w-100" alt="img n.@i.ToString()" />
</div>
}
}
</div>
<button class="carousel-control-prev" type="button" data-bs-target="#carouselHPFade" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">Previous</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#carouselHPFade" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">Next</span>
</button>
</div>
<h1>@ViewData["Title"]</h1>
<div class="row ps-5 pe-5">
<div class="col-12 col-md-9 col-lg-10">
<div>
@await Component.InvokeAsync("PromoProduct")
</div>
</div>
<div class="col-12 col-md-3 col-lg-2">
<div>
@await Component.InvokeAsync("HpBanner")
</div>
</div>
</div>
</div>
F5 per lanciare il Debug, per vedere più o meno questo.
Nella ViewComponent PromoProduct abbiamo utilizzato un loop Foreach per visualizzare il contenuto della Partial View _ProductCard.cshtml, nella quale viene creata una scheda con l’immagine e i dati del prodotto.
Terminiamo la scrittura della Vista parziale delle schede prodotti in promozione _ProductCard.cshtml, realizzando quattro nuove viste parziali da inserire nel folder “Shared”, che ci permetteranno di ridurre le linee di codice nella Card e nello stesso tempo di poterle riutilizzare in seguito:
_ProductQuote.cshtml
@model Product
<span class="text-danger">
@for (var n = 1; n <= Model.Quote; ++n)
{
<i class="bi-star-fill" style="font-size: 1.3rem;"></i>
}
@for (var n = Model.Quote; n < 5; ++n)
{
<i class="bi-star" style="font-size: 1.3rem;"></i>
}
</span>
_ProductStock.cshtml
@model Product
<p>
Disponibilità:
@if (Model.InStock > 0)
{
<span><b class="text-success">Immediata</b></span>
}
else
{
if (Model.InOrder > 0)
{
<span><b class="text-warning">In Ordine</b></span>
}
@else
{
<span><b class="text-danger">Non Disponibile</b></span>
}
}
</p>
_ProductPrice.cshtml
@model Product
@if (Model.PromoDiscount > 0)
{
<span>@Model.Price.Value:</span>
<span class="text-decoration-line-through">@Model.Price.Value</span>
<span>(-@Html.DisplayFor(modelItem => Model.PromoDiscount)%) <b>@(decimal.Round((Model.Price - ((Model.Price * Model.PromoDiscount) / 100)).Value, 2))</b></span>
}
else
{
<span>@ViewBag.Price: <b>@Model.Price.Value.ToString()</b></span>
}
_ProductCartWishSM.cshtml
<div>
<button class="btn btn-success"><i class="bi-cart-plus" style="font-size: 1.3rem;"></i></button>
<button class="btn btn-light"><i class="bi-heart" style="font-size: 1.3rem;"></i></button>
</div>
Per completare le modifiche inseriamo nella PartialView le chiamate alle Componenti appena create. Sostituiamo il codice del file “_ProductCard.cshtml” con quello che segue:
@model Product
<div class="card border-0 shadow mb-2">
<img src="~/images/products/@Model.Image" class="card-img-top" alt="@Model.Image">
<div class="card-body">
<h4>@Model.Name</h4>
<div class="card-text">
<partial name="_ProductQuote" />
<partial name="_ProductStock" />
<partial name="_ProductPrice" />
<partial name="_ProductCartWishSM" />
</div>
</div>
</div>
Eseguiamo il Debug.
Visualizzazione della card dei prodotti in promozione corretta, possiamo fermarci qui per ora. Nella prossima lezione ci occuperemo di definire una “Wish List” e un “Carrello” dove inserire i prodotti da acquistare.