sabato 29 aprile 2023

Ricerca , paginazione ed ordinamento in un applicazione Asp .Net Core

 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">

     <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 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> baseQuery = _context.Customers;
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 ovvero
orderby potrà valere CompanyName o ContactName ed ascending che varrà true o false ,
nell'action index possiamo gestire la combinazione dei quattro valori possibili in questo
modo:

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 fare
modifiche 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 *,Funzioni

in 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 scrivere
l'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 caso
in cui si debba gestire la paginazione dei risultati . Possiamo inserire il file cs sotto qualsiasi
directory ma per mantenere ordinato il progetto possiamo creare una directory
ViewComponents e sotto di essa aggiungere il file .cs ,se vogliamo creare ad esempio una
“PaginationBar” potremmo chiamare questo file PaginationBarViewComponent.cs , per
quanto concerne il file cshtml dobbiamo aggiungerlo sotto
Views\Shared\Components\PaginationBar\ ovvero avere
Views\Shared\Components\PaginationBar\Default.cshtml , in pratica il componente
PaginatoinBarViewComponent potrà utilizzare le view sotto la directory
Views\Shared\Components\PaginationBar ,se andiamo dentro la classe
PaginationBarViewComponent

public 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 della
view e se non passiamo nessun nome di view verrà utilizzata quella con nome Default


essendo il component sotto Shared possiamo utilizzarlo da qualsiasi
punto. Il file Default.cshtml contiene il seguente codice (ovvero “incorpora” il codice usato
sopra 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 cosa
si 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 il
minimo 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 appartiene
all'oggetto CustomersInputModel a sua volta appartenente all'oggetto
CustomersListViewModel .Il primo if ,nel caso in cui la pagina corrente non sia la prima
stampa,stampa un link “Precedente” il quale se cliccato setta il valore di page a
currentPage -1 oltre agli attributi asp-route-search , asp-route-orderby ed
asp-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 viene
settato lo sfondo blu . L'if successivo stampa il link “Successivo” nel caso in cui il numero
della 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 , @Model
utilizza 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 codice
html con un po' di codice c# ,in questo caso non scegliamo quale view utilizzare come nel
ViewComponent ma abbiamo una sola view ,che contiene l'eventuale codice c# , per passare
un modello ad una partial view possiamo usare l'attributo "model" del tag partial :

<partial name=”NomeDellaPartial” model=”ModelloDaUtilizzare” ></partial>

qui un po' di documentazione :

Crittografia e WCF per passare una password ( od una qualsiasi altra stringa (xml,json, etc.etc.) ) da un applicazione ad un' altra in relativa sicurezza

 Il codice che segue è da considerarsi in alpha e da non utilizzare in un ambiente di produzione , qui potete trovare il  "progetto&quo...