Parte 3, Razor Pagine con EF Core in ASP.NET Core - Ordina, Filtra, Paging

Di Tom Dykstra, Jeremy Likness e Jon P Smith

L'app Web Contoso University illustra come creare Razor app Web Pages usando EF Core e Visual Studio. Per informazioni sulla serie di esercitazioni, vedere la prima esercitazione.

Se si verificano problemi che non è possibile risolvere, scaricare l'app completata e confrontare tale codice con quello creato seguendo questa esercitazione.

In questa esercitazione si vedrà come aggiungere le funzionalità di ordinamento, filtro e suddivisione in pagine alla pagina Students.

La figura seguente illustra una pagina completata. Le intestazioni di colonna sono collegamenti selezionabili per ordinare la colonna. Fare clic più volte su un'intestazione di colonna per passare dall'ordinamento crescente a quello decrescente e viceversa.

Students index page

Aggiungere l'ordinamento

Sostituire il codice in Pages/Students/Index.cshtml.cs con il codice seguente per aggiungere l'ordinamento.

public class IndexModel : PageModel
{
    private readonly SchoolContext _context;
    public IndexModel(SchoolContext context)
    {
        _context = context;
    }

    public string NameSort { get; set; }
    public string DateSort { get; set; }
    public string CurrentFilter { get; set; }
    public string CurrentSort { get; set; }

    public IList<Student> Students { get; set; }

    public async Task OnGetAsync(string sortOrder)
    {
        // using System;
        NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
        DateSort = sortOrder == "Date" ? "date_desc" : "Date";

        IQueryable<Student> studentsIQ = from s in _context.Students
                                        select s;

        switch (sortOrder)
        {
            case "name_desc":
                studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
                break;
            case "Date":
                studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
                break;
            case "date_desc":
                studentsIQ = studentsIQ.OrderByDescending(s => s.EnrollmentDate);
                break;
            default:
                studentsIQ = studentsIQ.OrderBy(s => s.LastName);
                break;
        }

        Students = await studentsIQ.AsNoTracking().ToListAsync();
    }
}

Il codice precedente:

  • Richiede l'aggiunta di using System;.
  • Aggiunge le proprietà che contengono i parametri di ordinamento.
  • Modifica il nome della proprietà Student in Students.
  • Sostituisce il codice nel metodo OnGetAsync.

Il metodo OnGetAsync riceve un parametro sortOrder dalla stringa di query nell'URL. L'URL e la stringa di query vengono generati dall'helper tag di ancoraggio.

Il sortOrder parametro è Name o Date. Il sortOrder parametro è facoltativamente seguito da _desc per specificare l'ordine decrescente. Per impostazione predefinita, l'ordinamento è crescente.

Quando la pagina Index (Indice) viene richiesta dal collegamento Students (Studenti), non è presente alcuna stringa di query. Gli studenti vengono visualizzati in ordine crescente in base al cognome. L'ordine crescente per cognome è l'oggetto default nell'istruzione switch . Quando l'utente fa clic sul collegamento di un'intestazione di colonna, nel valore della stringa di query viene specificato il valore sortOrder appropriato.

NameSort e DateSort vengono usati dalla pagina per configurare i collegamenti ipertestuali dell'intestazione Razor di colonna con i valori della stringa di query appropriati:

NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";

Il codice usa l'operatore condizionale C# ?:. L'operatore ?: è un operatore ternario, che accetta tre operandi. La prima riga specifica che quando sortOrder è null o vuoto, NameSort è impostato su name_desc. Se sortOrdernon è Null o vuoto, NameSort è impostato su una stringa vuota.

Queste due istruzioni consentono alla pagina di impostare i collegamenti ipertestuali delle intestazioni di colonna come indicato di seguito:

Ordinamento corrente Collegamento ipertestuale cognome Collegamento ipertestuale data
Cognome in ordine crescente decrescente ascending
Cognome in ordine decrescente ascending ascending
Data in ordine crescente ascending decrescente
Data in ordine decrescente ascending ascending

Il metodo usa LINQ to Entities per specificare la colonna in base alla quale eseguire l'ordinamento. Il codice inizializza un elemento IQueryable<Student> prima dell'istruzione switch e lo modifica nell'istruzione switch:

IQueryable<Student> studentsIQ = from s in _context.Students
                                select s;

switch (sortOrder)
{
    case "name_desc":
        studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
        break;
    case "Date":
        studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
        break;
    case "date_desc":
        studentsIQ = studentsIQ.OrderByDescending(s => s.EnrollmentDate);
        break;
    default:
        studentsIQ = studentsIQ.OrderBy(s => s.LastName);
        break;
}

Students = await studentsIQ.AsNoTracking().ToListAsync();

Quando un oggetto IQueryable viene creato o modificato, non viene inviata alcuna query al database. La query non viene eseguita finché l'oggetto IQueryable non viene convertito in una raccolta. Gli elementi IQueryable vengono convertiti in una raccolta chiamando un metodo come ad esempio ToListAsync. Il codice IQueryable genera pertanto una singola query che non viene eseguita fino all'istruzione seguente:

Students = await studentsIQ.AsNoTracking().ToListAsync();

OnGetAsync può essere reso dettagliato con un numero elevato di colonne ordinabili. Per informazioni su un modo alternativo per scrivere il codice per questa funzionalità, vedere Usare LINQ dinamico per semplificare il codice nella versione MVC di questa serie di esercitazioni.

Sostituire il codice in Students/Index.cshtmlcon il codice seguente. Le modifiche sono evidenziate.

@page
@model ContosoUniversity.Pages.Students.IndexModel

@{
    ViewData["Title"] = "Students";
}

<h2>Students</h2>
<p>
    <a asp-page="Create">Create New</a>
</p>

<table class="table">
    <thead>
        <tr>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.NameSort">
                    @Html.DisplayNameFor(model => model.Students[0].LastName)
                </a>
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Students[0].FirstMidName)
            </th>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.DateSort">
                    @Html.DisplayNameFor(model => model.Students[0].EnrollmentDate)
                </a>
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Students)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.EnrollmentDate)
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

Il codice precedente:

  • Aggiunge collegamenti ipertestuali alle intestazioni di colonna LastName e EnrollmentDate.
  • Usa le informazioni contenute in NameSort e DateSort per impostare i collegamenti ipertestuali con i valori di ordinamento corrente.
  • Modifica l'intestazione di pagina da Index a Students.
  • Cambia Model.Student in Model.Students.

Per verificare il corretto funzionamento dell'ordinamento:

  • Eseguire l'app e selezionare la scheda Students (Studenti).
  • Fare clic sulle intestazioni di colonna.

Aggiungere un filtro

Per aggiungere un filtro alla pagina Students Index (Indice degli studenti):

  • Una casella di testo e un pulsante di invio viene aggiunto alla Razor pagina. La casella di testo specifica una stringa di ricerca sul nome o cognome.
  • Il modello di pagina viene aggiornato in modo da usare il valore della casella di testo.

Aggiornare il metodo OnGetAsync

Sostituire il codice in Students/Index.cshtml.cs con il codice seguente per aggiungere filtri:

public class IndexModel : PageModel
{
    private readonly SchoolContext _context;

    public IndexModel(SchoolContext context)
    {
        _context = context;
    }

    public string NameSort { get; set; }
    public string DateSort { get; set; }
    public string CurrentFilter { get; set; }
    public string CurrentSort { get; set; }

    public IList<Student> Students { get; set; }

    public async Task OnGetAsync(string sortOrder, string searchString)
    {
        NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
        DateSort = sortOrder == "Date" ? "date_desc" : "Date";

        CurrentFilter = searchString;
        
        IQueryable<Student> studentsIQ = from s in _context.Students
                                        select s;
        if (!String.IsNullOrEmpty(searchString))
        {
            studentsIQ = studentsIQ.Where(s => s.LastName.Contains(searchString)
                                   || s.FirstMidName.Contains(searchString));
        }

        switch (sortOrder)
        {
            case "name_desc":
                studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
                break;
            case "Date":
                studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
                break;
            case "date_desc":
                studentsIQ = studentsIQ.OrderByDescending(s => s.EnrollmentDate);
                break;
            default:
                studentsIQ = studentsIQ.OrderBy(s => s.LastName);
                break;
        }

        Students = await studentsIQ.AsNoTracking().ToListAsync();
    }
}

Il codice precedente:

  • Aggiunge il parametro searchString al metodo OnGetAsync e salva il valore del parametro nella proprietà CurrentFilter. Il valore della stringa di ricerca viene ricevuto da una casella di testo che verrà aggiunta nella sezione seguente.
  • Aggiunge una clausola Where all'istruzione LINQ. La clausola Where seleziona solo gli studenti il cui nome o cognome contiene la stringa di ricerca. L'istruzione LINQ viene eseguita solo se è presente un valore per la ricerca.

Confronto tra IQueryable e IEnumerable

Il codice chiama il metodo Where su un oggetto IQueryable e il filtro viene elaborato nel server. In alcuni scenari l'app potrebbe chiamare il metodo Where come metodo di estensione per una raccolta in memoria. Si supponga, ad esempio, che _context.Students le modifiche vengano apportate da EF CoreDbSet a un metodo del repository che restituisce una IEnumerable raccolta. Il risultato sarebbe in genere lo stesso, ma in alcuni casi potrebbe essere diverso.

Ad esempio, l'implementazione di .NET Framework di Contains esegue un confronto tra maiuscole e minuscole per impostazione predefinita. In SQL Server, la distinzione tra maiuscole e minuscole di Contains è determinata dall'impostazione delle regole di confronto dell'istanza di SQL Server. SQL Server usa come valore predefinito la non applicazione della distinzione tra maiuscole e minuscole. Per impostazione predefinita, SQLite fa distinzione tra maiuscole e minuscole. È possibile chiamare ToUpper per fare in modo che il test non applichi in modo esplicito la distinzione tra maiuscole e minuscole:

Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())`

Il codice precedente garantisce che il filtro non faccia distinzione tra maiuscole e minuscole anche se il metodo Where viene chiamato su un oggetto IEnumerable o viene eseguito in SQLite.

Quando viene chiamato Contains su una raccolta IEnumerable, viene usata l'implementazione di .NET Core. Quando viene chiamato Contains su un oggetto IQueryable, viene usata l'implementazione di database.

La chiamata di Contains su un oggetto IQueryable è in genere preferibile per motivi di prestazioni. Con IQueryable, il filtro viene applicato dal server di database. Se si crea per primo un IEnumerable, tutte le righe devono essere restituite dal server di database.

Si verifica una riduzione delle prestazioni per la chiamata di ToUpper. Il codice ToUpper aggiunge una funzione nella clausola WHERE dell'istruzione TSQL SELECT. La funzione aggiunta impedisce all'ottimizzazione di usare un indice. Dato che SQL viene installato con l'impostazione che non fa distinzione tra maiuscole e minuscole, è consigliabile evitare di chiamare ToUpper quando non è necessario.

Per altre informazioni, vedere How to use case-insensitive query with Sqlite provider (Come usare una query senza distinzione tra maiuscole e minuscole con il provider SQLite).

Aggiornare la Razor pagina

Sostituire il codice in Pages/Students/Index.cshtml per aggiungere un pulsante Cerca .

@page
@model ContosoUniversity.Pages.Students.IndexModel

@{
    ViewData["Title"] = "Students";
}

<h2>Students</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>

<form asp-page="./Index" method="get">
    <div class="form-actions no-color">
        <p>
            Find by name:
            <input type="text" name="SearchString" value="@Model.CurrentFilter" />
            <input type="submit" value="Search" class="btn btn-primary" /> |
            <a asp-page="./Index">Back to full List</a>
        </p>
    </div>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.NameSort">
                    @Html.DisplayNameFor(model => model.Students[0].LastName)
                </a>
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Students[0].FirstMidName)
            </th>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.DateSort">
                    @Html.DisplayNameFor(model => model.Students[0].EnrollmentDate)
                </a>
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Students)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.EnrollmentDate)
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

Il codice precedente usa l'helper tag<form> per aggiungere la casella di testo e il pulsante di ricerca. Per impostazione predefinita, l'helper tag <form> invia i dati del modulo con POST. Con POST, i parametri vengono passati nel corpo del messaggio HTTP e non nell'URL. Quando viene usato HTTP GET, i dati del modulo vengono passati nell'URL come stringhe di query. Il passaggio di dati con le stringhe di query consente agli utenti di inserire l'URL nei segnalibri. Le linee guida W3C consigliano di usare l'istruzione GET quando l'azione non risulta in un aggiornamento.

Eseguire il test dell'app:

  • Selezionare la scheda Students (Studenti) e immettere una stringa di ricerca. Se si usa SQLite, il filtro non fa distinzione tra maiuscole e minuscole solo se è stato implementato il codice ToUpper facoltativo illustrato in precedenza.

  • Seleziona Cerca.

Si noti che l'URL contiene la stringa di ricerca. Ad esempio:

https://localhost:5001/Students?SearchString=an

Se la pagina è inserita nei segnalibri, il segnalibro contiene l'URL alla pagina e la stringa di query SearchString. L'elemento method="get" nel tag form è ciò che ha determinato la generazione della stringa di query.

Attualmente, quando si seleziona un collegamento di ordinamento di un'intestazione di colonna, il valore del filtro dalla casella Search (Ricerca) viene perso. Il valore del filtro perso viene risolto nella sezione successiva.

Aggiungere la suddivisione in pagine

In questa sezione viene creata una classe PaginatedList per supportare la suddivisione in pagine. La classe PaginatedList usa le istruzioni Skip e Take per filtrare i dati sul server invece di recuperare tutte le righe della tabella. Nella figura seguente vengono illustrati i pulsanti di suddivisione in pagine.

Students index page with paging links

Creare la classe PaginatedList

Nella cartella del progetto, creare PaginatedList.cs con il codice seguente:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity
{
    public class PaginatedList<T> : List<T>
    {
        public int PageIndex { get; private set; }
        public int TotalPages { get; private set; }

        public PaginatedList(List<T> items, int count, int pageIndex, int pageSize)
        {
            PageIndex = pageIndex;
            TotalPages = (int)Math.Ceiling(count / (double)pageSize);

            this.AddRange(items);
        }

        public bool HasPreviousPage => PageIndex > 1;

        public bool HasNextPage => PageIndex < TotalPages;

        public static async Task<PaginatedList<T>> CreateAsync(
            IQueryable<T> source, int pageIndex, int pageSize)
        {
            var count = await source.CountAsync();
            var items = await source.Skip(
                (pageIndex - 1) * pageSize)
                .Take(pageSize).ToListAsync();
            return new PaginatedList<T>(items, count, pageIndex, pageSize);
        }
    }
}

Il metodo CreateAsync nel codice precedente accetta le dimensioni di pagina e il numero delle pagine e applica le istruzioni Skip e Take appropriate a IQueryable. Quando ToListAsync viene chiamato su IQueryable, restituisce un elenco contenente solo la pagina richiesta. Le proprietà HasPreviousPage e HasNextPage vengono usate per abilitare o disabilitare i pulsanti di suddivisione in pagine Previous (Indietro) e Next (Avanti).

Il metodo CreateAsync viene usato per creare l'elemento PaginatedList<T>. Un costruttore non può creare l'oggetto PaginatedList<T>, perché i costruttori non possono eseguire codice asincrono.

Aggiungere le dimensioni della pagina alla configurazione

Aggiungere PageSize al appsettings.jsonfile di configurazione :

{
  "PageSize": 3,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "SchoolContext": "Server=(localdb)\\mssqllocaldb;Database=CU-1;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}

Aggiungere il paging a IndexModel

Sostituire il codice in Students/Index.cshtml.cs per aggiungere il paging.

using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Students
{
    public class IndexModel : PageModel
    {
        private readonly SchoolContext _context;
        private readonly IConfiguration Configuration;

        public IndexModel(SchoolContext context, IConfiguration configuration)
        {
            _context = context;
            Configuration = configuration;
        }

        public string NameSort { get; set; }
        public string DateSort { get; set; }
        public string CurrentFilter { get; set; }
        public string CurrentSort { get; set; }

        public PaginatedList<Student> Students { get; set; }

        public async Task OnGetAsync(string sortOrder,
            string currentFilter, string searchString, int? pageIndex)
        {
            CurrentSort = sortOrder;
            NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
            DateSort = sortOrder == "Date" ? "date_desc" : "Date";
            if (searchString != null)
            {
                pageIndex = 1;
            }
            else
            {
                searchString = currentFilter;
            }

            CurrentFilter = searchString;

            IQueryable<Student> studentsIQ = from s in _context.Students
                                             select s;
            if (!String.IsNullOrEmpty(searchString))
            {
                studentsIQ = studentsIQ.Where(s => s.LastName.Contains(searchString)
                                       || s.FirstMidName.Contains(searchString));
            }
            switch (sortOrder)
            {
                case "name_desc":
                    studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
                    break;
                case "Date":
                    studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
                    break;
                case "date_desc":
                    studentsIQ = studentsIQ.OrderByDescending(s => s.EnrollmentDate);
                    break;
                default:
                    studentsIQ = studentsIQ.OrderBy(s => s.LastName);
                    break;
            }

            var pageSize = Configuration.GetValue("PageSize", 4);
            Students = await PaginatedList<Student>.CreateAsync(
                studentsIQ.AsNoTracking(), pageIndex ?? 1, pageSize);
        }
    }
}

Il codice precedente:

  • Modifica il tipo della proprietà Students da IList<Student> a PaginatedList<Student>.
  • Aggiunge l'indice della pagina, l'elemento sortOrder corrente e l'elemento currentFilter alla firma del metodo OnGetAsync.
  • Salva l'ordinamento nella CurrentSort proprietà .
  • Reimposta l'indice della pagina su 1 quando è presente una nuova stringa di ricerca.
  • Usa la classe PaginatedList per ottenere entità Student.
  • Imposta pageSize su 3 da Configuration, 4 se la configurazione non riesce.

Tutti i parametri ricevuti OnGetAsync da sono Null quando:

  • La pagina viene chiamata dal collegamento Students (Studenti).
  • L'utente non ha selezionato un collegamento di suddivisione in pagine o di ordinamento.

Se si seleziona un collegamento di suddivisione in pagine, la variabile dell'indice di pagina contiene il numero di pagina da visualizzare.

La CurrentSort proprietà fornisce alla pagina l'ordinamento Razor corrente. L'ordinamento corrente deve essere incluso nei collegamenti di suddivisione in pagine per mantenere l'ordinamento nella suddivisione in pagine.

La CurrentFilter proprietà fornisce alla Razor pagina la stringa di filtro corrente. Il valore CurrentFilter:

  • Deve essere incluso nei collegamenti di suddivisione in pagine per mantenere le impostazioni di filtro nella suddivisione in pagine.
  • Deve essere ripristinato nella casella di testo quando viene nuovamente visualizzata la pagina.

Se la stringa di ricerca viene modificata durante la suddivisione in pagine, la pagina viene reimpostata su 1. La pagina deve essere reimpostata su 1, poiché il nuovo filtro può comportare la visualizzazione di dati diversi. Quando viene immesso un valore di ricerca e si seleziona Submit (Invia):

  • La stringa di ricerca viene modificata.
  • Il parametro searchString non è Null.

Il metodo PaginatedList.CreateAsync converte la query degli studenti in una pagina singola di studenti in un tipo di raccolta che supporta la suddivisione in pagine. La singola pagina degli studenti viene passata alla Razor pagina.

I due punti interrogativi dopo pageIndex nella chiamata di PaginatedList.CreateAsync rappresentano l'operatore null-coalescing. L'operatore null-coalescing definisce un valore predefinito per un tipo nullable. L'espressione pageIndex ?? 1 restituisce il valore di pageIndex se ha un valore, in caso contrario restituisce 1.

Sostituire il codice in Students/Index.cshtml con il codice seguente. Le modifiche sono evidenziate:

@page
@model ContosoUniversity.Pages.Students.IndexModel

@{
    ViewData["Title"] = "Students";
}

<h2>Students</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>

<form asp-page="./Index" method="get">
    <div class="form-actions no-color">
        <p>
            Find by name: 
            <input type="text" name="SearchString" value="@Model.CurrentFilter" />
            <input type="submit" value="Search" class="btn btn-primary" /> |
            <a asp-page="./Index">Back to full List</a>
        </p>
    </div>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
                   asp-route-currentFilter="@Model.CurrentFilter">
                    @Html.DisplayNameFor(model => model.Students[0].LastName)
                </a>
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Students[0].FirstMidName)
            </th>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.DateSort"
                   asp-route-currentFilter="@Model.CurrentFilter">
                    @Html.DisplayNameFor(model => model.Students[0].EnrollmentDate)
                </a>
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Students)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.EnrollmentDate)
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

@{
    var prevDisabled = !Model.Students.HasPreviousPage ? "disabled" : "";
    var nextDisabled = !Model.Students.HasNextPage ? "disabled" : "";
}

<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Students.PageIndex - 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-primary @prevDisabled">
    Previous
</a>
<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Students.PageIndex + 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-primary @nextDisabled">
    Next
</a>

I collegamenti delle intestazioni di colonna usano la stringa di query per passare la stringa di ricerca corrente al metodo OnGetAsync:

<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
   asp-route-currentFilter="@Model.CurrentFilter">
    @Html.DisplayNameFor(model => model.Students[0].LastName)
</a>

I pulsanti di suddivisione in pagine vengono visualizzati dagli helper tag:


<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Students.PageIndex - 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-primary @prevDisabled">
    Previous
</a>
<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Students.PageIndex + 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-primary @nextDisabled">
    Next
</a>

Eseguire l'app e passare alla pagina degli studenti.

  • Per verificare che la suddivisione in pagine funzioni correttamente, fare clic sui collegamenti di suddivisione in pagine in ordinamenti diversi.
  • Per verificare che la suddivisione in pagine funzioni correttamente con l'ordinamento e il filtro, immettere una stringa di ricerca e provare nuovamente la suddivisione in pagine.

students index page with paging links

Raggruppamento

Questa sezione crea una About pagina che visualizza il numero di studenti registrati per ogni data di iscrizione. L'aggiornamento usa il raggruppamento e include i passaggi seguenti:

  • Creare un modello di visualizzazione per i dati usati dalla About pagina.
  • Aggiornare la About pagina per usare il modello di visualizzazione.

Creare il modello di visualizzazione

Creare una cartella Models/SchoolViewModels.

Creare SchoolViewModels/EnrollmentDateGroup.cs con il codice seguente:

using System;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models.SchoolViewModels
{
    public class EnrollmentDateGroup
    {
        [DataType(DataType.Date)]
        public DateTime? EnrollmentDate { get; set; }

        public int StudentCount { get; set; }
    }
}

Creare la Razor pagina

Creare un Pages/About.cshtml file con il codice seguente:

@page
@model ContosoUniversity.Pages.AboutModel

@{
    ViewData["Title"] = "Student Body Statistics";
}

<h2>Student Body Statistics</h2>

<table>
    <tr>
        <th>
            Enrollment Date
        </th>
        <th>
            Students
        </th>
    </tr>

    @foreach (var item in Model.Students)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.EnrollmentDate)
            </td>
            <td>
                @item.StudentCount
            </td>
        </tr>
    }
</table>

Creare il modello di pagina

Aggiornare il Pages/About.cshtml.cs file con il codice seguente:

using ContosoUniversity.Models.SchoolViewModels;
using ContosoUniversity.Data;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ContosoUniversity.Models;

namespace ContosoUniversity.Pages
{
    public class AboutModel : PageModel
    {
        private readonly SchoolContext _context;

        public AboutModel(SchoolContext context)
        {
            _context = context;
        }

        public IList<EnrollmentDateGroup> Students { get; set; }

        public async Task OnGetAsync()
        {
            IQueryable<EnrollmentDateGroup> data =
                from student in _context.Students
                group student by student.EnrollmentDate into dateGroup
                select new EnrollmentDateGroup()
                {
                    EnrollmentDate = dateGroup.Key,
                    StudentCount = dateGroup.Count()
                };

            Students = await data.AsNoTracking().ToListAsync();
        }
    }
}

L'istruzione LINQ raggruppa le entità di studenti per data di registrazione, calcola il numero di entità in ogni gruppo e archivia i risultati in una raccolta di oggetti di modello della visualizzazione EnrollmentDateGroup.

Eseguire l'app e passare alla pagina About (Informazioni). Il numero di studenti per ogni data di registrazione viene visualizzato in una tabella.

About page

Passaggi successivi

Nella prossima esercitazione, l'app usa le migrazioni per aggiornare il modello di dati.

In questa esercitazione viene aggiunta la funzionalità di ordinamento, filtro, suddivisione in pagine e raggruppamento.

La figura seguente illustra una pagina completata. Le intestazioni di colonna sono collegamenti selezionabili per ordinare la colonna. Facendo clic più volte su un'intestazione di colonna è possibile passare dall'ordinamento crescente a quello decrescente e viceversa.

Students index page

Se si verificano problemi che non si è in grado di risolvere, scaricare l'app completa.

Aggiungere l'ordinamento alla pagina Index (Indice)

Aggiungere stringhe a Students/Index.cshtml.csPageModel per contenere i parametri di ordinamento:

public class IndexModel : PageModel
{
    private readonly SchoolContext _context;

    public IndexModel(SchoolContext context)
    {
        _context = context;
    }

    public string NameSort { get; set; }
    public string DateSort { get; set; }
    public string CurrentFilter { get; set; }
    public string CurrentSort { get; set; }

Aggiornare Students/Index.cshtml.csOnGetAsync con il codice seguente:

public async Task OnGetAsync(string sortOrder)
{
    NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    DateSort = sortOrder == "Date" ? "date_desc" : "Date";

    IQueryable<Student> studentIQ = from s in _context.Student
                                    select s;

    switch (sortOrder)
    {
        case "name_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            studentIQ = studentIQ.OrderBy(s => s.LastName);
            break;
    }

    Student = await studentIQ.AsNoTracking().ToListAsync();
}

Il codice precedente riceve un parametro sortOrder dalla stringa di query nell'URL. L'URL (inclusa la stringa di query) viene generato dall'helper tag di ancoraggio

Il sortOrder parametro è "Name" o "Date". Il sortOrder parametro è facoltativamente seguito da "_desc" per specificare l'ordine decrescente. Per impostazione predefinita, l'ordinamento è crescente.

Quando la pagina Index (Indice) viene richiesta dal collegamento Students (Studenti), non è presente alcuna stringa di query. Gli studenti vengono visualizzati in ordine crescente in base al cognome. L'ordine crescente in base al cognome è l'impostazione predefinita (caso di fallthrough) nell'istruzione switch. Quando l'utente fa clic sul collegamento di un'intestazione di colonna, nel valore della stringa di query viene specificato il valore sortOrder appropriato.

NameSort e DateSort vengono usati dalla pagina per configurare i collegamenti ipertestuali dell'intestazione Razor di colonna con i valori della stringa di query appropriati:

public async Task OnGetAsync(string sortOrder)
{
    NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    DateSort = sortOrder == "Date" ? "date_desc" : "Date";

    IQueryable<Student> studentIQ = from s in _context.Student
                                    select s;

    switch (sortOrder)
    {
        case "name_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            studentIQ = studentIQ.OrderBy(s => s.LastName);
            break;
    }

    Student = await studentIQ.AsNoTracking().ToListAsync();
}

Il codice seguente contiene l'operatore condizionale ?: di C#:

NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";

La prima riga specifica che quando sortOrder è null o vuoto, NameSort è impostato su "name_desc". Se sortOrder non è null o vuoto, NameSort è impostato su una stringa vuota.

?: operator è noto anche come operatore ternario.

Queste due istruzioni consentono alla pagina di impostare i collegamenti ipertestuali delle intestazioni di colonna come indicato di seguito:

Ordinamento corrente Collegamento ipertestuale cognome Collegamento ipertestuale data
Cognome in ordine crescente decrescente ascending
Cognome in ordine decrescente ascending ascending
Data in ordine crescente ascending decrescente
Data in ordine decrescente ascending ascending

Il metodo usa LINQ to Entities per specificare la colonna in base alla quale eseguire l'ordinamento. Il codice inizializza un elemento IQueryable<Student> prima dell'istruzione switch e lo modifica nell'istruzione switch:

public async Task OnGetAsync(string sortOrder)
{
    NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    DateSort = sortOrder == "Date" ? "date_desc" : "Date";

    IQueryable<Student> studentIQ = from s in _context.Student
                                    select s;

    switch (sortOrder)
    {
        case "name_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            studentIQ = studentIQ.OrderBy(s => s.LastName);
            break;
    }

    Student = await studentIQ.AsNoTracking().ToListAsync();
}

Quando viene creato o modificato un elemento IQueryable, non viene inviata alcuna query al database. La query non viene eseguita finché l'oggetto IQueryable non viene convertito in una raccolta. Gli elementi IQueryable vengono convertiti in una raccolta chiamando un metodo come ad esempio ToListAsync. Il codice IQueryable genera pertanto una singola query che non viene eseguita fino all'istruzione seguente:

Student = await studentIQ.AsNoTracking().ToListAsync();

OnGetAsync può essere reso dettagliato con un numero elevato di colonne ordinabili.

Sostituire il codice in Students/Index.cshtmlcon il codice evidenziato seguente:

@page
@model ContosoUniversity.Pages.Students.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>
<p>
    <a asp-page="Create">Create New</a>
</p>

<table class="table">
    <thead>
        <tr>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.NameSort">
                    @Html.DisplayNameFor(model => model.Student[0].LastName)
                </a>
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Student[0].FirstMidName)
            </th>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.DateSort">
                    @Html.DisplayNameFor(model => model.Student[0].EnrollmentDate)
                </a>
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Student)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.EnrollmentDate)
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

Il codice precedente:

  • Aggiunge collegamenti ipertestuali alle intestazioni di colonna LastName e EnrollmentDate.
  • Usa le informazioni contenute in NameSort e DateSort per impostare i collegamenti ipertestuali con i valori di ordinamento corrente.

Per verificare il corretto funzionamento dell'ordinamento:

  • Eseguire l'app e selezionare la scheda Students (Studenti).
  • Fare clic su Last Name (Cognome).
  • Fare clic su Enrollment Date (Data di iscrizione).

Per comprendere meglio il codice:

  • In Students/Index.cshtml.csimpostare un punto di interruzione su switch (sortOrder).
  • Aggiungere un'espressione di controllo per NameSort e DateSort.
  • In Students/Index.cshtmlimpostare un punto di interruzione su @Html.DisplayNameFor(model => model.Student[0].LastName).

Eseguire il debugger.

Aggiungere una casella di ricerca alla pagina Students Index (Indice degli studenti)

Per aggiungere un filtro alla pagina Students Index (Indice degli studenti):

  • Una casella di testo e un pulsante di invio viene aggiunto alla Razor pagina. La casella di testo specifica una stringa di ricerca sul nome o cognome.
  • Il modello di pagina viene aggiornato in modo da usare il valore della casella di testo.

Aggiungere la funzionalità di filtro al metodo Index

Aggiornare Students/Index.cshtml.csOnGetAsync con il codice seguente:

public async Task OnGetAsync(string sortOrder, string searchString)
{
    NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    DateSort = sortOrder == "Date" ? "date_desc" : "Date";
    CurrentFilter = searchString;

    IQueryable<Student> studentIQ = from s in _context.Student
                                    select s;
    if (!String.IsNullOrEmpty(searchString))
    {
        studentIQ = studentIQ.Where(s => s.LastName.Contains(searchString)
                               || s.FirstMidName.Contains(searchString));
    }

    switch (sortOrder)
    {
        case "name_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            studentIQ = studentIQ.OrderBy(s => s.LastName);
            break;
    }

    Student = await studentIQ.AsNoTracking().ToListAsync();
}

Il codice precedente:

  • Aggiunge il parametro searchString al metodo OnGetAsync. Il valore della stringa di ricerca viene ricevuto da una casella di testo che verrà aggiunta nella sezione seguente.
  • Aggiunge una clausola Where all'istruzione LINQ. La clausola Where seleziona solo gli studenti il cui nome o cognome contiene la stringa di ricerca. L'istruzione LINQ viene eseguita solo se è presente un valore per la ricerca.

Nota: il codice precedente chiama il metodo Where su un oggetto IQueryable e il filtro viene elaborato nel server. In alcuni scenari l'app potrebbe chiamare il metodo Where come metodo di estensione per una raccolta in memoria. Si supponga, ad esempio, che _context.Students le modifiche vengano apportate da EF CoreDbSet a un metodo del repository che restituisce una IEnumerable raccolta. Il risultato sarebbe in genere lo stesso, ma in alcuni casi potrebbe essere diverso.

Ad esempio, l'implementazione di .NET Framework di Contains esegue un confronto tra maiuscole e minuscole per impostazione predefinita. In SQL Server, la distinzione tra maiuscole e minuscole di Contains è determinata dall'impostazione delle regole di confronto dell'istanza di SQL Server. SQL Server usa come valore predefinito la non applicazione della distinzione tra maiuscole e minuscole. È possibile chiamare ToUpper per fare in modo che il test non applichi in modo esplicito la distinzione tra maiuscole e minuscole:

Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())

Il codice precedente assicura che i risultati applichino la distinzione tra maiuscole e minuscole se il codice viene modificato per usare IEnumerable. Quando viene chiamato Contains su una raccolta IEnumerable, viene usata l'implementazione di .NET Core. Quando viene chiamato Contains su un oggetto IQueryable, viene usata l'implementazione di database. La restituzione di un elemento IEnumerable da un repository può compromettere in modo significativo le prestazioni:

  1. Tutte le righe vengono restituite dal server di database.
  2. Il filtro viene applicato a tutte le righe restituite nell'applicazione.

Si verifica una riduzione delle prestazioni per la chiamata di ToUpper. Il codice ToUpper aggiunge una funzione nella clausola WHERE dell'istruzione TSQL SELECT. La funzione aggiunta impedisce all'ottimizzazione di usare un indice. Dato che SQL viene installato con l'impostazione che non fa distinzione tra maiuscole e minuscole, è consigliabile evitare di chiamare ToUpper quando non è necessario.

Aggiungere una casella di ricerca alla pagina Student Index (Indice degli studenti)

In Pages/Students/Index.cshtmlaggiungere il codice evidenziato seguente per creare un pulsante Di ricerca e un riquadro diversi.

@page
@model ContosoUniversity.Pages.Students.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>

<form asp-page="./Index" method="get">
    <div class="form-actions no-color">
        <p>
            Find by name:
            <input type="text" name="SearchString" value="@Model.CurrentFilter" />
            <input type="submit" value="Search" class="btn btn-default" /> |
            <a asp-page="./Index">Back to full List</a>
        </p>
    </div>
</form>

<table class="table">

Il codice precedente usa l'helper tag<form> per aggiungere la casella di testo e il pulsante di ricerca. Per impostazione predefinita, l'helper tag <form> invia i dati del modulo con POST. Con POST, i parametri vengono passati nel corpo del messaggio HTTP e non nell'URL. Quando viene usato HTTP GET, i dati del modulo vengono passati nell'URL come stringhe di query. Il passaggio di dati con le stringhe di query consente agli utenti di inserire l'URL nei segnalibri. Le linee guida W3C consigliano di usare l'istruzione GET quando l'azione non risulta in un aggiornamento.

Eseguire il test dell'app:

  • Selezionare la scheda Students (Studenti) e immettere una stringa di ricerca.
  • Seleziona Cerca.

Si noti che l'URL contiene la stringa di ricerca.

http://localhost:5000/Students?SearchString=an

Se la pagina è inserita nei segnalibri, il segnalibro contiene l'URL alla pagina e la stringa di query SearchString. L'elemento method="get" nel tag form è ciò che ha determinato la generazione della stringa di query.

Attualmente, quando si seleziona un collegamento di ordinamento di un'intestazione di colonna, il valore del filtro dalla casella Search (Ricerca) viene perso. Il valore del filtro perso viene risolto nella sezione successiva.

Aggiungere la funzionalità di suddivisione in pagine alla pagina Student Index (Indice degli studenti)

In questa sezione viene creata una classe PaginatedList per supportare la suddivisione in pagine. La classe PaginatedList usa le istruzioni Skip e Take per filtrare i dati sul server invece di recuperare tutte le righe della tabella. Nella figura seguente vengono illustrati i pulsanti di suddivisione in pagine.

Students index page with paging links

Nella cartella del progetto, creare PaginatedList.cs con il codice seguente:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity
{
    public class PaginatedList<T> : List<T>
    {
        public int PageIndex { get; private set; }
        public int TotalPages { get; private set; }

        public PaginatedList(List<T> items, int count, int pageIndex, int pageSize)
        {
            PageIndex = pageIndex;
            TotalPages = (int)Math.Ceiling(count / (double)pageSize);

            this.AddRange(items);
        }

        public bool HasPreviousPage => PageIndex > 1;

        public bool HasNextPage => PageIndex < TotalPages;

        public static async Task<PaginatedList<T>> CreateAsync(
            IQueryable<T> source, int pageIndex, int pageSize)
        {
            var count = await source.CountAsync();
            var items = await source.Skip(
                (pageIndex - 1) * pageSize)
                .Take(pageSize).ToListAsync();
            return new PaginatedList<T>(items, count, pageIndex, pageSize);
        }
    }
}

Il metodo CreateAsync nel codice precedente accetta le dimensioni di pagina e il numero delle pagine e applica le istruzioni Skip e Take appropriate a IQueryable. Quando ToListAsync viene chiamato su IQueryable, restituisce un elenco contenente solo la pagina richiesta. Le proprietà HasPreviousPage e HasNextPage vengono usate per abilitare o disabilitare i pulsanti di suddivisione in pagine Previous (Indietro) e Next (Avanti).

Il metodo CreateAsync viene usato per creare l'elemento PaginatedList<T>. Un costruttore non può creare l'oggetto PaginatedList<T>, poiché i costruttori non possono eseguire codice asincrono.

Aggiungere la funzionalità di suddivisione in pagine al metodo Index

In Students/Index.cshtml.csaggiornare il tipo di Student da IList<Student> a PaginatedList<Student>:

public PaginatedList<Student> Student { get; set; }

Aggiornare Students/Index.cshtml.csOnGetAsync con il codice seguente:

public async Task OnGetAsync(string sortOrder,
    string currentFilter, string searchString, int? pageIndex)
{
    CurrentSort = sortOrder;
    NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    DateSort = sortOrder == "Date" ? "date_desc" : "Date";
    if (searchString != null)
    {
        pageIndex = 1;
    }
    else
    {
        searchString = currentFilter;
    }

    CurrentFilter = searchString;

    IQueryable<Student> studentIQ = from s in _context.Student
                                    select s;
    if (!String.IsNullOrEmpty(searchString))
    {
        studentIQ = studentIQ.Where(s => s.LastName.Contains(searchString)
                               || s.FirstMidName.Contains(searchString));
    }
    switch (sortOrder)
    {
        case "name_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            studentIQ = studentIQ.OrderBy(s => s.LastName);
            break;
    }

    int pageSize = 3;
    Student = await PaginatedList<Student>.CreateAsync(
        studentIQ.AsNoTracking(), pageIndex ?? 1, pageSize);
}

Il codice precedente aggiunge l'indice della pagina, l'elemento sortOrder corrente e l'elemento currentFilter alla firma del metodo.

public async Task OnGetAsync(string sortOrder,
    string currentFilter, string searchString, int? pageIndex)

Tutti i parametri sono Null quando:

  • La pagina viene chiamata dal collegamento Students (Studenti).
  • L'utente non ha selezionato un collegamento di suddivisione in pagine o di ordinamento.

Se si seleziona un collegamento di suddivisione in pagine, la variabile dell'indice di pagina contiene il numero di pagina da visualizzare.

CurrentSort fornisce la Razor pagina con l'ordinamento corrente. L'ordinamento corrente deve essere incluso nei collegamenti di suddivisione in pagine per mantenere l'ordinamento nella suddivisione in pagine.

CurrentFilter fornisce alla Razor pagina la stringa di filtro corrente. Il valore CurrentFilter:

  • Deve essere incluso nei collegamenti di suddivisione in pagine per mantenere le impostazioni di filtro nella suddivisione in pagine.
  • Deve essere ripristinato nella casella di testo quando viene nuovamente visualizzata la pagina.

Se la stringa di ricerca viene modificata durante la suddivisione in pagine, la pagina viene reimpostata su 1. La pagina deve essere reimpostata su 1, poiché il nuovo filtro può comportare la visualizzazione di dati diversi. Quando viene immesso un valore di ricerca e si seleziona Submit (Invia):

  • La stringa di ricerca viene modificata.
  • Il parametro searchString non è Null.
if (searchString != null)
{
    pageIndex = 1;
}
else
{
    searchString = currentFilter;
}

Il metodo PaginatedList.CreateAsync converte la query degli studenti in una pagina singola di studenti in un tipo di raccolta che supporta la suddivisione in pagine. La singola pagina degli studenti viene passata alla Razor pagina.

Student = await PaginatedList<Student>.CreateAsync(
    studentIQ.AsNoTracking(), pageIndex ?? 1, pageSize);

I due punti interrogativi in PaginatedList.CreateAsync rappresentano l'operatore null-coalescing. L'operatore null-coalescing definisce un valore predefinito per un tipo nullable. L'espressione (pageIndex ?? 1) restituisce il valore di pageIndex se ha un valore. Se pageIndex non ha un valore, restituisce 1.

Aggiornare il markup in Students/Index.cshtml. Le modifiche sono evidenziate:

@page
@model ContosoUniversity.Pages.Students.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>

<form asp-page="./Index" method="get">
    <div class="form-actions no-color">
        <p>
            Find by name: <input type="text" name="SearchString" value="@Model.CurrentFilter" />
            <input type="submit" value="Search" class="btn btn-default" /> |
            <a asp-page="./Index">Back to full List</a>
        </p>
    </div>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
                   asp-route-currentFilter="@Model.CurrentFilter">
                    @Html.DisplayNameFor(model => model.Student[0].LastName)
                </a>
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Student[0].FirstMidName)
            </th>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.DateSort"
                   asp-route-currentFilter="@Model.CurrentFilter">
                    @Html.DisplayNameFor(model => model.Student[0].EnrollmentDate)
                </a>
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Student)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.EnrollmentDate)
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

@{
    var prevDisabled = !Model.Student.HasPreviousPage ? "disabled" : "";
    var nextDisabled = !Model.Student.HasNextPage ? "disabled" : "";
}

<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Student.PageIndex - 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-default @prevDisabled">
    Previous
</a>
<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Student.PageIndex + 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-default @nextDisabled">
    Next
</a>

I collegamenti delle intestazioni di colonna usano la stringa di query per passare la stringa di ricerca corrente al metodo OnGetAsync in modo che l'utente possa procedere all'ordinamento all'interno dei risultati di filtro:

<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
   asp-route-currentFilter="@Model.CurrentFilter">
    @Html.DisplayNameFor(model => model.Student[0].LastName)
</a>

I pulsanti di suddivisione in pagine vengono visualizzati dagli helper tag:


<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Student.PageIndex - 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-default @prevDisabled">
    Previous
</a>
<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Student.PageIndex + 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-default @nextDisabled">
    Next
</a>

Eseguire l'app e passare alla pagina degli studenti.

  • Per verificare che la suddivisione in pagine funzioni correttamente, fare clic sui collegamenti di suddivisione in pagine in ordinamenti diversi.
  • Per verificare che la suddivisione in pagine funzioni correttamente con l'ordinamento e il filtro, immettere una stringa di ricerca e provare nuovamente la suddivisione in pagine.

students index page with paging links

Per comprendere meglio il codice:

  • In Students/Index.cshtml.csimpostare un punto di interruzione su switch (sortOrder).
  • Aggiungere un'espressione di controllo per NameSort, DateSort, CurrentSort e Model.Student.PageIndex.
  • In Students/Index.cshtmlimpostare un punto di interruzione su @Html.DisplayNameFor(model => model.Student[0].LastName).

Eseguire il debugger.

Aggiornare la pagina About (Informazioni) per visualizzare le statistiche degli studenti

In questo passaggio viene Pages/About.cshtml aggiornato per visualizzare il numero di studenti iscritti per ogni data di iscrizione. L'aggiornamento usa il raggruppamento e include i passaggi seguenti:

  • Creare un modello di visualizzazione per i dati usati dalla pagina About (Informazioni).
  • Aggiornare la pagina About in modo che usi il modello di visualizzazione.

Creare il modello di visualizzazione

Creare una cartella SchoolViewModels nella cartella Models.

Nella cartella SchoolViewModels aggiungere un oggetto EnrollmentDateGroup.cs con il codice seguente:

using System;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models.SchoolViewModels
{
    public class EnrollmentDateGroup
    {
        [DataType(DataType.Date)]
        public DateTime? EnrollmentDate { get; set; }

        public int StudentCount { get; set; }
    }
}

Aggiornare il modello di pagina About (Informazioni)

I modelli Web in ASP.NET Core 2.2 non includono la pagina About (Informazioni). Se si usa ASP.NET Core 2.2, creare la pagina Informazioni.Razor

Aggiornare il Pages/About.cshtml.cs file con il codice seguente:

using ContosoUniversity.Models.SchoolViewModels;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ContosoUniversity.Models;

namespace ContosoUniversity.Pages
{
    public class AboutModel : PageModel
    {
        private readonly SchoolContext _context;

        public AboutModel(SchoolContext context)
        {
            _context = context;
        }

        public IList<EnrollmentDateGroup> Student { get; set; }

        public async Task OnGetAsync()
        {
            IQueryable<EnrollmentDateGroup> data =
                from student in _context.Student
                group student by student.EnrollmentDate into dateGroup
                select new EnrollmentDateGroup()
                {
                    EnrollmentDate = dateGroup.Key,
                    StudentCount = dateGroup.Count()
                };

            Student = await data.AsNoTracking().ToListAsync();
        }
    }
}

L'istruzione LINQ raggruppa le entità di studenti per data di registrazione, calcola il numero di entità in ogni gruppo e archivia i risultati in una raccolta di oggetti di modello della visualizzazione EnrollmentDateGroup.

Modificare la pagina Informazioni Razor

Sostituire il codice nel Pages/About.cshtml file con il codice seguente:

@page
@model ContosoUniversity.Pages.AboutModel

@{
    ViewData["Title"] = "Student Body Statistics";
}

<h2>Student Body Statistics</h2>

<table>
    <tr>
        <th>
            Enrollment Date
        </th>
        <th>
            Students
        </th>
    </tr>

    @foreach (var item in Model.Student)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.EnrollmentDate)
            </td>
            <td>
                @item.StudentCount
            </td>
        </tr>
    }
</table>

Eseguire l'app e passare alla pagina About (Informazioni). Il numero di studenti per ogni data di registrazione viene visualizzato in una tabella.

Se si verificano problemi che non si è in grado di risolvere, scaricare l'app completa per questa fase.

About page

Risorse aggiuntive

Nella prossima esercitazione, l'app usa le migrazioni per aggiornare il modello di dati.