In molti progetti web sono necessarie funzioni di ricerca , di ordinamento dei risultati e di paginazione degli stessi ,vediamo come sia possibile costruire queste funzionalità . Creiamo un progetto App Web Asp .NET core (Model-View-Controller) utilizzando Visual Studio 2022 e chiamiamolo ad esempio Funzioni .
Ora aggiorniamo i package del progetto in modo da poter creare un modello dati partendo da un database esistente utilizzando l'istruzione Scaffold-DbContext , questo modo di creazione del modello è detto Database-first
eseguiamo le seguenti istruzioni (nella Console di Gestione pacchetti):
Install-Package Microsoft.EntityFrameworkCore.Tools
Install-Package Microsoft.EntityFrameworkCore.SqlServer
scarichiamo lo script per creare e popolare il database Northwind (https://github.com/microsoft/sql-server-samples/tree/master/samples/databases/northwind-pubs) ,eseguiamo lo script instnwnd.sql in Sql Server Management Studio ,
ora possiamo utilizzare l'istruzione Scaffold-DbContext in questo modo :
Scaffold-DbContext "Data Source=(localdb)\mssqllocaldb;Initial Catalog=Northwind;Integrated Security=True" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -ContextDir Context -Context NorthwindContext
l'esecuzione di quest'istruzione comporta la creazione della directory Context ed il popolamento della directory Models ,che prima conteneva solo il file ErrorViewModel , con tutte le entità che vanno a costruire il modello del db Northwind.
Dentro la directory Context troviamo il il dbcontext di nome NorthwindContext ,abbiamo passato il nome del context passandolo al parametro “-Context” e la directory dove scriverlo al parametro ”-ContextDir” dell'istruzione Scaffold-DbContext ,segue l'elenco dei modelli in Models
il file NorthwindContext contiene tutti i DbSet delle entities che vanno a formare il modello e nel metodo OnModelCreating possiamo trovare la “definizione” di tutte le entità e le relative relazioni .
Possiamo ora creare un controller che gestisca le operazioni su una identità ad esempio Customers , in visual studio possiamo selezionare la directory Controllers ,scegliere aggiungi --> controller e scegliere “Controller MVC con visualizzazioni che usa Entity Framework” e scegliere aggiungi ,avremo la seguente schermata
scegliamo come classe Customer come DbContext NorthwindContext e come nome lasciamo quello proposto di default da visual studio ovvero CustomersController
clicchiamo su aggiungi ,ora avremo dei cambiamenti nel progetto ,sotto la dir controllers ora avremo anche il CustomersController ,aggiungiamo in Program.cs dopo builder.Services.AddControllersWithViews();
e prima di
var app=builder.Build();
builder.Services.AddDbContext<NorthwindContext>(options =>options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
in modo da attenere :
builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<NorthwindContext>(options =>options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
var app = builder.Build();
ora aggiungiamo la DefaultConnection al file appsettings.json in modo da ottenere
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=Northwind;Trusted_Connection=True;MultipleActiveResultSets=true"
},
etc.etc
a questo punto proviamo ad eseguire in debug il progetto cliccando su F5 , ora dovremmo ottenere
ora cambiamo l'indirizzo del browser in https://localhost:7098/Customers ed abbiamo
ovvero viene chiamata l'action Index del controller Customers ,questo perchè la querystring contiene solo Customers ma non essendo specificata una action il map controller route definisce come default Index ,
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
oltre ad essere stato aggiunto un file CustomersController sotto la dir Controllers alcuni file sono aggiunti sotto Views ovvero una directory Customers e sotto ad essa le view :
le action chiamano le relative view passando i dati che la view stessa deve utilizzare , ad esempio la action Index di Customers passa un oggetto List<Customer> alla view e la view attende infatti un
Ienumerable<Funzioni.Models.Customer> .
Aggiungiamo nel file index.html un form che punti a Customers/Index con un textbox di input di nome "search"
<div class="row">
<a asp-action="Create">Create New</a>
</div>
<div class="col-md-4 d-flex align-items-center">
<form asp-controller="Customers" asp-action="Index" method="get">
<div class="input-group">
<input type="text" class="form-control" name="search" placeholder="Cerca company name..." />
<div class="input-group-append">
<button class="input-group-text">Cerca</i></button>
</div>
</div>
</form>
</div>
</div>
modifichiamo di conseguenza la action index del controller Customers :
public async Task<IActionResult> Index(string search="") {
IEnumerable<Customer> customers = _context.Customers.Where(customer=> customer.CompanyName.Contains(search));
return View(customers);
}
proviamo la nuova Index cercando ad esempio le compagnie che abbiano nel companyname la stringa "dos"
ed otteniamo :
Per paginare i risultati e gestire l'ordinamento dovremo gestire altre variabili rispetto a quelle che consideriamo adesso ovvero :
1)dobbiamo utilizzare una variabile che mantenga valorizzato il textbox search con l'ultimo testo scelto
2) occorre una variabile che contenga il campo sul quale applicare l'orderby
3) è necessaria una variabile per indicare se l'ordinamento è ascending o descending
4) occorre indicare quale page è quella corrente
questi 4 campi possono essere gestiti come 4 proprietà di una classe :
public class CustomersInputModel
public string Search { get; }
public string OrderBy { get; }
public bool Ascending { get; }
public int Page { get; }
public CustomersInputModel(string search,string orderby,bool ascending,int page) {
this.Search = search;
this.OrderBy = orderby;
this.Ascending = ascending;
this.Page = page;
}
}
occorrono altri dati per la paginazione ad esempio di un elenco di oggetti Customers :
a) l'elenco degli oggetti Customers (customers)
b) quanti elementi per pagina dovranno essere utilizzati (LimitForPage)
c) quanti elementi abbiamo in totale (CountedItems) ,
la classe CustomersListViewModel che segue contiene un oggetto CustomersInputModel e le ultime tre variabili:
public class CustomersListViewModel
IConfiguration _configuration;
public IEnumerable<ProgettoDue.Models.Customer> customers { get; set; }
public CustomersInputModel customersInputModel { get; set; }
public int CountedItems { get; set; }
public int LimitForPage { get; set; }
public CustomersListViewModel(IConfiguration conf)
{
_configuration= conf;
IConfigurationSection s = _configuration.GetSection("Paginazione");
int limit = s.GetValue<int>("perpagina");
this.LimitForPage = limit;
}
}
il valore di limit è ricavato dall'elemento “perpagina” della sezione Paginazione di appsettings.json
"Paginazione": {
"perpagina": "10"
}
il valore di CountedItems è valorizzato nell'action Index utilizzando il metodo Count() sulla collezione di elementi Customer ritornati dalla Where :
CustomersListViewModel model = new CustomersListViewModel(_configuration);
IQueryable<Customer> queryLinq = baseQuery
.Where(customer => customer.CompanyName.Contains(search));
model.CountedItems = queryLinq.Count();
dove model è l'istanza della classe CustomersListViewModel ,
per brevità gestiamo solo i campi CompanyName e ContactName ,a questo punto la pagina Index.cshtml avrà come model CustomersListViewModel e si dovrebbe presentare così' :
@model CustomersListViewModel
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<div class="row">
<div class="col-md-8">
<a asp-action="Create">Create New</a>
</div>
<div class="col-md-4 d-flex align-items-center">
<form asp-controller="Customers" asp-action="Index" method="get">
<div class="input-group">
<input type="text" class="form-control" name="search" placeholder="Cerca..." value="@Model.customersInputModel.Search" />
<div class="input-group-append">
<button class="input-group-text">Cerca</i></button>
</div>
</div>
</form>
</div>
</div>
<div class="row">
<div class="col-md-6"><a asp-route-orderby="CompanyName" asp-route-ascending="@(Model.customersInputModel.OrderBy=="CompanyName" ? !Model.customersInputModel.Ascending:true)" asp-route-search="@Model.customersInputModel.Search">CompanyName</a></div>
<div class="col-md-6"><a asp-route-orderby="ContactName" asp-route-ascending="@(Model.customersInputModel.OrderBy=="ContactName" ? !Model.customersInputModel.Ascending:true)" asp-route-search="@Model.customersInputModel.Search">ContactName</a></div>
</div>
Inizio nota ordinamento:
da notare l'operatore ternario: @(Model.customersInputModel.OrderBy=="ContactName" ?
!Model.customersInputModel.Ascending:true) "“equivalente”" di un if else ma più compatto ,
qui la documentazione .
La prima volta che viene eseguita l'action Index del controller CustomersController essa viene eseguita con i parametri di default ovvero :
public async Task<IActionResult> Index(string search = "",int page=1,string orderby="CompanyName",bool ascending=true)
{…......
…........
}
e quindi viene valorizzato "ascending" a False poiché c'è l'operatore “!” e Model.customersInputModel.Ascending di default vale “true” , il valore iniziale di orderby settato come valore di default in Index è “CompanyName” ,
il tag “a” CompanyName varrà https://localhost:7190/Customers?orderby=CompanyName&ascending=False
per l'ordinamento avremo quindi due variabili ognuna con due valori possibili ovveroorderby potrà valere CompanyName o ContactName ed ascending che varrà true o false ,nell'action index possiamo gestire la combinazione dei quattro valori possibili in questomodo:IQueryable<Customer> baseQuery = _context.Customers;
baseQuery= (orderby, ascending) switch{
("CompanyName",true) => baseQuery.OrderBy(customer => customer.CompanyName),
("CompanyName",false)=> baseQuery.OrderByDescending(customer =>customer.CompanyName),
("ContactName",true) => baseQuery.OrderBy(customer => customer.ContactName),
("ContactName",false) => baseQuery.OrderByDescending(customer =>customer.ContactName),
_=> baseQuery
};
Fine nota ordinamento
Stampiamo l'elenco delle aziende:
@foreach (var item in Model.customers) {
<div class="row"> <div class="col-md-6"> @Html.DisplayFor(modelItem => item.CompanyName)
</div> <div class="col-md-6"> @Html.DisplayFor(modelItem => item.ContactName)
</div> </div> }
@{ int totalPages =(int)Math.Ceiling(Model.CountedItems / (decimal)Model.LimitForPage);int currentPage = Model.customersInputModel.Page; }
<nav aria-label="paginazione"> <ul class="pagination">
@if(currentPage>1) {
<li class="page-item"><a class="page-link" asp-route-page="@(currentPage-1)"
asp-route-search="@Model.customersInputModel.Search"asp-route-orderby="@Model.customersInputModel.OrderBy" asp-route-ascending="@Model.customersInputModel.Ascending">Precedente </a></li> }@for(int p=Math.Max(1,currentPage-4); p<=Math.Min(totalPages,currentPage+4);p++)
{
if(p==currentPage) {
<li class="page-item active"><a class="page-link" asp-route-page="@p"
asp-route-search="@Model.customersInputModel.Search"
asp-route-orderby="@Model.customersInputModel.OrderBy"
asp-route-ascending="@Model.customersInputModel.Ascending">@p</a></li> } else {
<li class="page-item"><a class="page-link" asp-route-page="@p"
asp-route-search="@Model.customersInputModel.Search"
asp-route-orderby="@Model.customersInputModel.OrderBy"
asp-route-ascending="@Model.customersInputModel.Ascending">@p</a></li> } }
@if(currentPage < totalPages) { <li class="page-item "><a class="page-link" asp-route-page="@(currentPage+1)"
asp-route-search="@Model.customersInputModel.Search"asp-route-orderby="@Model.customersInputModel.OrderBy"
asp-route-ascending="@Model.customersInputModel.Ascending">Successivo </a></li> }</ul> </nav>
il tag nav racchiude la logica della paginazione .Può essere utile realizzare dei componenti per riutilizzare della logica e faremodifiche al comportamento in modo centralizzato,per realizzare dei “componenti” asp .net core prevede 3 possibili strade :1)TagHelper : occorre un solo file cs
2) ViewComponent si compone di due file : uno è una view razor (cshtml) ,l'altro è
una classe(cs)
3) partial view : sono composte solo da un solo file cshtml
Vediamo i 3 possibili metodi nel dettaglio:1) un TagHelper potrebbe essere "test" a cui passo due numeri e mi ritorna il prodotto :
creiamo una classe il cui nome sarà TestTagHelper ovvero al nome seguirà il postfisso
'TagHelper' , la classe deve ereditare da TagHelper e dobbiamo fare l'override del metodo
Process , segue la classe che definisce il taghelper test :public class TestTagHelper:TagHelper
{
public TestTagHelper()
{ }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
HtmlString val1 = (HtmlString)context.AllAttributes["valore1"].Value;
HtmlString val2 = (HtmlString)context.AllAttributes["valore2"].Value;
int valore3 = System.Convert.ToInt32(val1.Value);
int valore4 = System.Convert.ToInt32(val2.Value);
output.Content.AppendHtml("il prodotto è :" + (valore3*valore4).ToString());
}
}
per utilizzare i tag helper dobbiamo aggiungere un riferimento ai tag helper del progetto
nel file _ViewImports.cshtml ,se ad esempio la dll generata dal progetto si chiama
“Funzioni” il file _ViewImports dovrebbe contenere qualcosa di simile:@using Funzioni@using Funzioni.Models@addTagHelper *,Microsoft.AspNetCore.Mvc.TagHelpers@addTagHelper *,Funzioniin fondo alla Index.html aggiungete
<test valore1="3" valore2="4"></test>
ricompilate ed eseguite ed otterrete :
il taghelper non è molto comodo nel caso sia necessario scrivere un po' di html poichè
scrivendo in un contesto puramente csharp non abbiamo nessuna facilitazione nello scriverel'html
2) un component view può essere la scelta giusta nel caso in cui si debba scrivere un po' piùdi html rispetto al caso precedente ,un component view potrebbe risultare adeguato nel casoin cui si debba gestire la paginazione dei risultati . Possiamo inserire il file cs sotto qualsiasidirectory ma per mantenere ordinato il progetto possiamo creare una directoryViewComponents e sotto di essa aggiungere il file .cs ,se vogliamo creare ad esempio una“PaginationBar” potremmo chiamare questo file PaginationBarViewComponent.cs , perquanto concerne il file cshtml dobbiamo aggiungerlo sottoViews\Shared\Components\PaginationBar\ ovvero avereViews\Shared\Components\PaginationBar\Default.cshtml , in pratica il componentePaginatoinBarViewComponent potrà utilizzare le view sotto la directoryViews\Shared\Components\PaginationBar ,se andiamo dentro la classePaginationBarViewComponentpublic class PaginationBarViewComponent : ViewComponent{public IViewComponentResult Invoke(CustomersListViewModel model){return View(model);}}possiamo osservare che il return View permette di scegliere anche di passare il nome dellaview e se non passiamo nessun nome di view verrà utilizzata quella con nome Defaultpunto. Il file Default.cshtml contiene il seguente codice (ovvero “incorpora” il codice usatosopra nella pagina Index.cshtml ) :@model CustomersListViewModel@{int totalPages = (int)Math.Ceiling(Model.CountedItems / (decimal)Model.LimitPerPage);int currentPage = Model.customersInputModel.Page;}<nav aria-label="paginazione"> <ul class="pagination">
@if(currentPage>1) {
<li class="page-item"><a class="page-link" asp-route-page="@(currentPage-1)"
asp-route-search="@Model.customersInputModel.Search"asp-route-orderby="@Model.customersInputModel.OrderBy" asp-route-ascending="@Model.customersInputModel.Ascending">Precedente </a></li> }@for(int p=Math.Max(1,currentPage-4); p<=Math.Min(totalPages,currentPage+4);p++)
{
if(p==currentPage) {
<li class="page-item active"><a class="page-link" asp-route-page="@p"
asp-route-search="@Model.customersInputModel.Search"
asp-route-orderby="@Model.customersInputModel.OrderBy"
asp-route-ascending="@Model.customersInputModel.Ascending">@p</a></li> } else {
<li class="page-item"><a class="page-link" asp-route-page="@p"
asp-route-search="@Model.customersInputModel.Search"
asp-route-orderby="@Model.customersInputModel.OrderBy"
asp-route-ascending="@Model.customersInputModel.Ascending">@p</a></li> } }
@if(currentPage < totalPages) { <li class="page-item "><a class="page-link" asp-route-page="@(currentPage+1)"
asp-route-search="@Model.customersInputModel.Search"asp-route-orderby="@Model.customersInputModel.OrderBy"
asp-route-ascending="@Model.customersInputModel.Ascending">Successivo </a></li> }</ul> </nav>la pagina contiene un riferimento alla classe CustomersListViewModel , per prima cosasi ottiene il numero totale delle pagine dividendo il numero degli items (i customers)per il numero degli elementi per pagina e tramite la funzione Math.Ceiling si ottiene ilminimo numero intero superiore al risultato(https://learn.microsoft.com/en-us/dotnet/api/system.math.ceiling?view=net-8.0)
,successivamente ricaviamo la pagina corrente dal valore di Page che appartieneall'oggetto CustomersInputModel a sua volta appartenente all'oggettoCustomersListViewModel .Il primo if ,nel caso in cui la pagina corrente non sia la primastampa,stampa un link “Precedente” il quale se cliccato setta il valore di page acurrentPage -1 oltre agli attributi asp-route-search , asp-route-orderby edasp-route-ascending .Il for costruisce i link per le singole pagine ,nel caso in cui la pagina“p” sia la currentPage class viene settato con la class a “page.item active” ovvero vienesettato lo sfondo blu . L'if successivo stampa il link “Successivo” nel caso in cui il numerodella pagina corrente sia inferiore al numero totale delle pagine:per aggiungere la barra di navigazione delle pagine dobbiamo aggiungere<vc:pagination-bar model="@Model"></vc:pagination-bar>nella pagina e nel punto in cui vogliamo sia visualizzato , il tag vc stà per viewcomponent , @Modelutilizza il model dichiarato per la pagina ovvero @model CustomersListViewModel , pagination-barè il ""tag"" del view component ricavato da PaginationBar e scritto in kebab-case(https://it.wikipedia.org/wiki/Kebab_case)
3) La partial view è composta solo da un file cshtml ed è adatta a riutilizzare molto codicehtml con un po' di codice c# ,in questo caso non scegliamo quale view utilizzare come nelViewComponent ma abbiamo una sola view ,che contiene l'eventuale codice c# , per passareun modello ad una partial view possiamo usare l'attributo "model" del tag partial :<partial name=”NomeDellaPartial” model=”ModelloDaUtilizzare” ></partial>qui un po' di documentazione :