Teil 3: Razor-Seiten mit EF Core in ASP.NET Core – Sortieren, Filtern und Paging

Von Tom Dykstra, Jeremy Likness und Jon P. Smith

Die Web-App Contoso University veranschaulicht, wie Razor-Seiten-Web-Apps mit EF Core und Visual Studio erstellt werden können. Informationen zu den Tutorials finden Sie im ersten Tutorial.

Wenn Probleme auftreten, die Sie nicht beheben können, laden Sie die vollständige App herunter, und vergleichen Sie diesen Code mit dem Code, den Sie anhand des Tutorials erstellt haben.

In diesem Tutorial werden Funktionen zum Sortieren und Filtern sowie für Paging zur Student-Seiten hinzugefügt.

Die folgende Abbildung zeigt eine fertige Seite. Die Spaltenüberschriften sind Links, die zum Sortieren der Spalte angeklickt werden können. Wiederholtes Klicken auf eine Spaltenüberschrift schaltet zwischen aufsteigender und absteigender Sortierreihenfolge um.

Students index page

Hinzufügen von Sortierung

Ersetzen Sie den Code in Pages/Students/Index.cshtml.cs durch den folgenden Code, um Sortierung hinzuzufügen.

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();
    }
}

Der vorangehende Code:

  • Er erfordert das Hinzufügen von using System;.
  • Fügt Eigenschaften hinzu, die die Sortierparameter enthalten.
  • Ändert den Namen der Student-Eigenschaft in Students.
  • Ersetzt den Code in der OnGetAsync-Methode.

Die OnGetAsync-Methode empfängt einen sortOrder-Parameter aus der Abfragezeichenfolge in der URL. Die URL und die Abfragezeichenfolge werden vom Anchor-Taghilfsprogramm generiert.

Der sortOrder-Parameter ist entweder Name oder Date. Dem Parameter sortOrder folgt optional _desc zur Angabe einer absteigenden Reihenfolge. Standardmäßig wird eine aufsteigende Sortierreihenfolge verwendet.

Wenn die Indexseite über den Link Studenten angefordert wird, gibt es keine Abfragezeichenfolge. Die Studenten werden in aufsteigender Reihenfolge nach Nachnamen angezeigt. Die aufsteigende Reihenfolge nach Nachnamen ist default in der switch-Anweisung. Wenn der Benutzer auf einen Link in einer Spaltenüberschrift klickt, wird der entsprechende sortOrder-Wert im Abfragezeichenfolgenwert bereitgestellt.

NameSort und DateSort werden auf der Razor-Seite verwendet, um die Hyperlinks in den Spaltenüberschriften mit den entsprechenden Abfragezeichenfolgenwerten zu konfigurieren:

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

Der Code verwendet den bedingten C#-Operator ?:. Der Operator ?: ist ein ternärer Operator (er nimmt drei Operanden an). Die erste Zeile gibt an, dass wenn sortOrder NULL oder leer ist, NameSort auf name_desc festgelegt wird. Wenn sortOrdernicht NULL oder leer ist, wird NameSort auf eine leere Zeichenfolge festgelegt.

Durch diese beiden Anweisungen können auf der Seite die Hyperlinks in den Spaltenüberschriften wie folgt festgelegt werden:

Aktuelle Sortierreihenfolge Hyperlink „Nachname“ Hyperlink „Datum“
Nachname (aufsteigend) descending ascending
Nachname (absteigend) ascending ascending
Datum (aufsteigend) ascending descending
Datum (absteigend) ascending ascending

Die Methode gibt über LINQ to Entities die Spalte an, nach der sortiert werden soll. Der Code initialisiert das Objekt IQueryable<Student> vor der Switch-Anweisung und ändert es in die Switch-Anweisung:

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();

Wenn ein Objekt vom Typ IQueryable erstellt oder geändert wird, wird keine Abfrage an die Datenbank gesendet. Die Abfrage wird erst dann ausgeführt, wenn das Objekt IQueryable in eine Sammlung konvertiert wurde. IQueryable werden in eine Sammlung konvertiert, indem eine Methode wie z.B. ToListAsync aufgerufen wird. Aus diesem Grund führt der IQueryable-Code zu einer einzelnen Abfrage, die bis zu folgender Anweisung nicht ausgeführt wird:

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

OnGetAsync könnte mit einer Vielzahl von sortierbaren Spalten ausführlich werden. Informationen zu einer alternativen Möglichkeit zum Programmieren dieser Funktionalität finden Sie unter Verwenden von dynamischem LINQ zum Vereinfachen von Code in der MVC-Version dieser Tutorialreihe.

Ersetzen Sie den Code in Students/Index.cshtml durch folgenden Code. Die Änderungen werden hervorgehoben.

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

Der vorangehende Code:

  • Fügt Hyperlinks zu den Spaltenüberschriften LastName und EnrollmentDate hinzu.
  • Verwendet die Informationen in NameSort und DateSort zum Einrichten von Hyperlinks mit den Werten der aktuellen Sortierreihenfolge.
  • Ändert die Seitenüberschrift aus „Index“ in „Students“.
  • Ändert Model.Student in Model.Students.

So überprüfen Sie die Funktionsfähigkeit der Sortierung:

  • Führen Sie die App aus, und klicken Sie auf die Registerkarte Studenten.
  • Klicken Sie auf die Spaltenüberschriften.

Hinzufügen von Filterung

So fügen Sie Filter zur Indexseite „Studenten“ hinzu:

  • Ein Textfeld und die Schaltfläche „Senden“ werden zur Razor Page hinzugefügt. Im Textfeld wird eine Suchzeichenfolge für den Vor- oder Nachnamen bereitgestellt.
  • Das Seitenmodell wird aktualisiert, damit der Wert im Textfeld verwendet wird.

Aktualisieren der OnGetAsync-Methode

Ersetzen Sie den Code in Students/Index.cshtml.cs durch den folgenden Code, um Filterung hinzuzufügen:

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();
    }
}

Der vorangehende Code:

  • Fügt der OnGetAsync-Methode den searchString-Parameter hinzu und speichert den Parameterwert in der CurrentFilter-Eigenschaft. Der Suchzeichenfolgenwert wird aus einem Textfeld empfangen, das im nächsten Abschnitt hinzugefügt wird.
  • Fügt der LINQ-Anweisung eine Where-Klausel hinzu. In der Where-Klausel werden nur die Studenten ausgewählt, deren Vor- oder Nachname die Suchzeichenfolge enthält. Die LINQ-Anweisung wird nur dann ausgeführt, wenn ein Wert vorhanden ist, nach dem gesucht werden soll.

IQueryable im Vergleich zu IEnumerable

Der Code ruft die Methode Where in einem IQueryable-Objekt auf, und der Filter wird auf dem Server verarbeitet. In einigen Szenarios ruft die App möglicherweise die Methode Where als Erweiterungsmethode für eine Auflistung im Speicher auf. Angenommen, _context.Students wird beispielsweise von EF CoreDbSet in eine Repositorymethode geändert, die eine IEnumerable-Sammlung zurückgibt. Das Ergebnis wäre normalerweise identisch, aber in einigen Fällen kann es unterschiedlich ausfallen.

Bei der .NET Framework-Implementierung von Contains wird beispielsweise standardmäßig ein Vergleich unter Beachtung der Groß-/Kleinschreibung vorgenommen. In SQL Server wird die Unterscheidung von Groß-/Kleinschreibung bei Contains durch die Einstellung für die Sortierung der SQL Server-Instanz bestimmt. Bei SQL Server wird die Groß-/Kleinschreibung standardmäßig nicht beachtet. Für SQLite wird standardmäßig zwischen Groß-/Kleinschreibung unterschieden. ToUpper könnte aufgerufen werden, wenn die Groß-/Kleinschreibung bei der Durchführung des Tests explizit nicht beachtet werden soll:

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

Der vorstehende Code würde sicherstellen, dass der Filter nicht zwischen Groß-/Kleinschreibung unterscheidet, auch wenn die Where-Methode für IEnumerable aufgerufen oder für SQLite ausgeführt wird.

Wenn Contains in einer IEnumerable-Sammlung verwendet wird, wird die .NET Core-Implementierung verwendet. Wenn Contains in einem IQueryable-Objekt aufgerufen wird, wird die Datenbankimplementierung verwendet.

Der Aufruf von Contains für IQueryable ist in der Regel aus Leistungsgründen vorzuziehen. Mit IQueryable wird die Filterung vom Datenbankserver ausgeführt. Wenn zuerst ein IEnumerable erstellt wird, müssen alle Zeilen vom Datenbankserver zurückgegeben werden.

Beim Aufrufen von ToUpper kommt es zu Leistungseinbußen. Der ToUpper-Code in der WHERE-Klausel der TSQL SELECT-Anweisung eine Funktion hinzu. Durch die hinzugefügte Funktion wird verhindert, dass der Optimierer einen Index verwendet. Da in SQL die Groß-/Kleinschreibung beachtet wird, sollte ToUpper möglichst nicht aufgerufen werden, wenn dies nicht notwendig ist.

Weitere Informationen finden Sie unter Verwenden von Abfragen ohne Berücksichtigung der Groß-/Kleinschreibung mit dem SQLite-Anbieter.

Aktualisieren der Razor-Seite

Ersetzen Sie den Code in Pages/Students/Index.cshtml, um eine Schaltfläche Search (Suchen) hinzuzufügen.

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

Der vorangehende Code verwendet das <form>-Taghilfsprogramm, um das Suchtextfeld und die Schaltfläche hinzuzufügen. Das <form>-Taghilfsprogramm sendet Formulardaten standardmäßig mit einer POST-Anforderung. Mit der POST-Anforderung werden die Parameter im HTTP-Nachrichtentext und nicht in der URL übergeben. Bei Verwendung einer HTTP GET-Anforderung werden die Formulardaten als Abfragezeichenfolgen in der URL übergeben. Durch die Übergabe der Daten mit Abfragezeichenfolgen können Benutzer die URL als Lesezeichen speichern. In den W3C-Richtlinien wird die Verwendung einer GET-Anforderung empfohlen, wenn die Aktion nicht zu einem Update führt.

Testen der App:

  • Klicken Sie auf die Registerkarte Studenten, und geben Sie eine Suchzeichenfolge ein. Wenn Sie SQLite verwenden, wird für den Filter nur denn nicht zwischen Groß-/Kleinschreibung unterschieden, wenn Sie den zuvor gezeigten optionalen ToUpper-Code implementiert haben.

  • Klicken Sie auf Suchen.

Beachten Sie, dass die URL die Suchzeichenfolge enthält. Zum Beispiel:

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

Wenn die Seite als Lesezeichen gespeichert wird, enthält das Lesezeichen die URL der Seite und die SearchString-Abfragezeichenfolge. Durch method="get" im form-Tag wurde die Abfragezeichenfolge generiert.

Aktuell geht der Filterwert aus dem Feld Suchen verloren, wenn ein Link zur Sortierung der Spaltenüberschrift ausgewählt wird. Das Problem des verlorenen Filterwerts wird im nächsten Abschnitt behoben.

Hinzufügen von Paging

In diesem Abschnitt wird eine PaginatedList-Klasse zur Unterstützung von Paging erstellt. Die PaginatedList-Klasse verwendet Skip- und Take-Anweisungen zum Filtern von Daten auf dem Server, statt alle Zeilen der Tabelle abzurufen. Die folgende Abbildung zeigt die Pagingschaltflächen.

Students index page with paging links

Erstellen der PaginatedList-Klasse

Erstellen Sie im Projektordner PaginatedList.cs mit folgendem Code:

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);
        }
    }
}

Die Methode CreateAsync im vorangehenden Code übernimmt die Seitengröße und die Seitenzahl und wendet die entsprechenden Skip- und Take-Anweisungen auf das Objekt IQueryable an. Wenn ToListAsync im Objekt IQueryable aufgerufen wird, wird eine Liste zurückgegeben, die nur die angeforderte Seite enthält. Die Eigenschaften HasPreviousPage und HasNextPage dienen zum Aktivieren oder Deaktivieren der Schaltflächen Zurück und Weiter für das Paging.

Die Methode CreateAsync dient zum Erstellen des Objekts PaginatedList<T>. Ein Konstruktor kann das Objekt PaginatedList<T> nicht erstellen. Konstruktoren können keinen asynchronen Code ausführen.

Hinzufügen der Seitengröße zur Konfiguration

Fügen Sie PageSize der appsettings.jsonKonfigurationsdatei hinzu:

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

Hinzufügen von Paging zu IndexModel

Ersetzen Sie den Code in Students/Index.cshtml.cs, um Paging hinzuzufügen.

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);
        }
    }
}

Der vorangehende Code:

  • Ändert den Typ der Students-Eigenschaft aus IList<Student> in PaginatedList<Student>.
  • Fügt den Seitenindex, die aktuelle sortOrder und den currentFilter zur OnGetAsync-Methodensignatur hinzu.
  • Er speichert die Sortierreihenfolge in der CurrentSort-Eigenschaft.
  • Setzt den Seitenindex auf 1 zurück, wenn eine neue Suchzeichenfolge vorhanden ist.
  • Verwendet die PaginatedList-Klasse zum Abrufen von Student-Entitäten.
  • Legt pageSize auf 3 aus Konfiguration fest, 4m wenn die Konfiguration fehlschlägt.

Alle Parameter, die OnGetAsync empfängt, sind NULL, wenn:

  • Die Seite über den Link Studenten aufgerufen wird.
  • Der Benutzer auf keinen Link für das Paging oder die Sortierung geklickt hat.

Wenn auf einen Paging-Link geklickt wird, enthält die Seitenindexvariable die anzuzeigende Seitenzahl.

Die CurrentSort-Eigenschaft stellt die Razor-Seite mit der aktuellen Sortierreihenfolge bereit. Die aktuelle Sortierreihenfolge muss in den Paging-Links enthalten sein, damit die Sortierreihenfolge während des Pagings beibehalten wird.

Die CurrentFilter-Eigenschaft stellt die Razor-Seite mit der aktuellen Filterzeichenfolge bereit. Der CurrentFilter-Wert:

  • Muss in den Paging-Links enthalten sein, damit die Filtereinstellungen während des Pagings beibehalten werden.
  • Muss im Textfeld wiederhergestellt werden, wenn die Seite erneut angezeigt wird.

Wenn die Suchzeichenfolge während des Pagings geändert wird, wird die Seite auf 1 zurückgesetzt. Die Seite muss auf 1 zurückgesetzt werden, da der neue Filter andere Daten anzeigen kann. Folgendes gilt, wenn ein Suchwert eingegeben und auf Senden geklickt wird:

  • Die Suchzeichenfolge wird geändert.
  • Der Parameter searchString ist nicht NULL.

Die Methode PaginatedList.CreateAsync konvertiert die Studentenabfrage in eine einzelne Studentenseite in einem Sammlungstyp, der das Paging unterstützt. Diese einzelne Studentenseite wird an die Razor Page übergeben.

Die zwei Fragezeichen nach pageIndex im PaginatedList.CreateAsync-Aufruf stellen den NULL-Sammeloperator dar. Der NULL-Sammeloperator definiert einen Standardwert für einen auf NULL festlegbaren Typ. Der Ausdruck pageIndex ?? 1 gibt den Wert pageIndex zurück, wenn er über einen Wert verfügt, andernfalls wird 1 zurückgegeben.

Ersetzen Sie den Code in Students/Index.cshtml durch folgenden Code. Die Änderungen werden hervorgehoben:

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

In den Links der Spaltenüberschriften wird die Abfragezeichenfolge verwendet, um die aktuelle Suchzeichenfolge an die Methode OnGetAsync zu übergeben:

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

Die Pagingschaltflächen werden durch Taghilfsprogramme angezeigt:


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

Führen Sie die App aus, und navigieren Sie zur Studentenseite.

  • Klicken Sie in verschiedenen Sortierreihenfolgen auf die Paging-Links, um sicherzustellen, dass das Paging funktioniert.
  • Geben Sie eine Suchzeichenfolge ein und versuchen Sie, das Paging durchzuführen, um sicherzustellen, dass das Paging bei Sortier- und Filtervorgängen ordnungsgemäß funktioniert.

students index page with paging links

Gruppierung

In diesem Abschnitt wird eine Seite „About“ (Info) erstellt, auf der angezeigt wird, wie viele Studenten sich für jedes Registrierungsdatum registriert haben. Bei dem Update wird eine Gruppierung verwendet, und es umfasst die folgenden Schritte:

  • Erstellen Sie ein Ansichtsmodells für die auf der Seite About (Info) verwendeten Daten.
  • Aktualisieren Sie die About-Seite, um das Ansichtsmodell zu verwenden.

Erstellen des Ansichtsmodells

Erstellen Sie einen Ordner Models/SchoolViewModels.

Erstellen Sie SchoolViewModels/EnrollmentDateGroup.cs mit dem folgenden Code:

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; }
    }
}

Erstellen der Razor-Seite

Erstellen Sie eine Datei Pages/About.cshtml mit dem folgenden Code:

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

Erstellen des Seitenmodells

Aktualisieren Sie die Datei Pages/About.cshtml.cs mit dem folgenden Code:

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();
        }
    }
}

Die LINQ-Anweisung gruppiert die Studentenentitäten nach Anmeldedatum, berechnet die Anzahl der Entitäten in jeder Gruppe und speichert die Ergebnisse in einer Sammlung von EnrollmentDateGroup-Ansichtsmodellobjekten.

Führen Sie die App aus, und navigieren Sie zur Seite „Info“. Die Anzahl der Studenten für die jeweiligen Anmeldedatumswerte wird in einer Tabelle angezeigt.

About page

Nächste Schritte

Im nächsten Tutorial verwendet die App Migrationen zum Aktualisieren des Datenmodells.

In diesem Tutorial werden die Funktionen zum Sortieren, Filtern, Gruppieren und Paging hinzugefügt.

Die folgende Abbildung zeigt eine fertige Seite. Die Spaltenüberschriften sind Links, die zum Sortieren der Spalte angeklickt werden können. Durch wiederholtes Klicken auf eine Spaltenüberschrift wird zwischen aufsteigender und absteigender Sortierreihenfolge gewechselt.

Students index page

Wenn nicht zu lösende Probleme auftreten, laden Sie die fertige App herunter.

Hinzufügen einer Sortierung zur Indexseite

Fügen Sie dem Students/Index.cshtml.csPageModel Zeichenfolgen hinzu, damit die Sortierungsparameter darin enthalten sind:

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; }

Aktualisieren Sie Students/Index.cshtml.csOnGetAsync mit dem folgenden Code:

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();
}

Der vorangehende Code erhält den Parameter sortOrder aus der Abfragezeichenfolge in der URL. Die URL (einschließlich der Abfragezeichenfolge) wird vom Anchor-Taghilfsprogramm generiert.

Der Parameter sortOrder entspricht entweder „Name“ oder „Datum“. Dem Parameter sortOrder folgt optional „_desc“ zur Angabe einer absteigenden Reihenfolge. Standardmäßig wird eine aufsteigende Sortierreihenfolge verwendet.

Wenn die Indexseite über den Link Studenten angefordert wird, gibt es keine Abfragezeichenfolge. Die Studenten werden in aufsteigender Reihenfolge nach Nachnamen angezeigt. Die aufsteigende Reihenfolge nach Nachnamen (FallThrough-Fall) ist in der switch-Anweisung die Standardeinstellung. Wenn der Benutzer auf einen Link in einer Spaltenüberschrift klickt, wird der entsprechende sortOrder-Wert im Abfragezeichenfolgenwert bereitgestellt.

NameSort und DateSort werden auf der Razor-Seite verwendet, um die Hyperlinks in den Spaltenüberschriften mit den entsprechenden Abfragezeichenfolgenwerten zu konfigurieren:

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();
}

Der folgende Code enthält den bedingten C#-Operator „?:“:

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

Die erste Zeile gibt an, dass wenn sortOrder NULL oder leer ist, NameSort auf „name_desc“ festgelegt ist. Wenn sortOrdernicht NULL oder leer ist, wird NameSort auf eine leere Zeichenfolge festgelegt.

?: operator ist auch als ternärer Operator bekannt.

Durch diese beiden Anweisungen können auf der Seite die Hyperlinks in den Spaltenüberschriften wie folgt festgelegt werden:

Aktuelle Sortierreihenfolge Hyperlink „Nachname“ Hyperlink „Datum“
Nachname (aufsteigend) descending ascending
Nachname (absteigend) ascending ascending
Datum (aufsteigend) ascending descending
Datum (absteigend) ascending ascending

Die Methode gibt über LINQ to Entities die Spalte an, nach der sortiert werden soll. Der Code initialisiert das Objekt IQueryable<Student> vor der Switch-Anweisung und ändert es in die Switch-Anweisung:

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();
}

Wenn ein Objekt vom Typ IQueryable erstellt oder geändert wird, wird keine Abfrage an die Datenbank gesendet. Die Abfrage wird erst dann ausgeführt, wenn das Objekt IQueryable in eine Sammlung konvertiert wurde. IQueryable werden in eine Sammlung konvertiert, indem eine Methode wie z.B. ToListAsync aufgerufen wird. Aus diesem Grund führt der IQueryable-Code zu einer einzelnen Abfrage, die bis zu folgender Anweisung nicht ausgeführt wird:

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

OnGetAsync könnte mit einer Vielzahl von sortierbaren Spalten ausführlich werden.

Ersetzen Sie den Code in Students/Index.cshtml durch folgenden hervorgehobenen Code:

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

Der vorangehende Code:

  • Fügt Hyperlinks zu den Spaltenüberschriften LastName und EnrollmentDate hinzu.
  • Verwendet die Informationen in NameSort und DateSort zum Einrichten von Hyperlinks mit den Werten der aktuellen Sortierreihenfolge.

So überprüfen Sie die Funktionsfähigkeit der Sortierung:

  • Führen Sie die App aus, und klicken Sie auf die Registerkarte Studenten.
  • Klicken Sie auf Nachname.
  • Klicken Sie auf Anmeldedatum.

So erhalten Sie ein besseres Verständnis des Codes:

  • Legen Sie in Students/Index.cshtml.cs einen Breakpoint bei switch (sortOrder) fest.
  • Fügen Sie für NameSort und DateSort ein Überwachungselement hinzu.
  • Legen Sie in Students/Index.cshtml einen Breakpoint bei @Html.DisplayNameFor(model => model.Student[0].LastName) fest.

Führen Sie den Debugger schrittweise aus.

Hinzufügen eines Suchfelds zur Studentenindexseite

So fügen Sie Filter zur Indexseite „Studenten“ hinzu:

  • Ein Textfeld und die Schaltfläche „Senden“ werden zur Razor Page hinzugefügt. Im Textfeld wird eine Suchzeichenfolge für den Vor- oder Nachnamen bereitgestellt.
  • Das Seitenmodell wird aktualisiert, damit der Wert im Textfeld verwendet wird.

Hinzufügen der Filterfunktion zur Indexmethode

Aktualisieren Sie Students/Index.cshtml.csOnGetAsync mit dem folgenden Code:

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();
}

Der vorangehende Code:

  • Fügt den Parameter searchString zur Methode OnGetAsync hinzu. Der Suchzeichenfolgenwert wird aus einem Textfeld empfangen, das im nächsten Abschnitt hinzugefügt wird.
  • Hat eine Where-Klausel zur LINQ-Anweisung hinzugefügt. In der Where-Klausel werden nur die Studenten ausgewählt, deren Vor- oder Nachname die Suchzeichenfolge enthält. Die LINQ-Anweisung wird nur dann ausgeführt, wenn ein Wert vorhanden ist, nach dem gesucht werden soll.

Hinweis: Der vorangehende Code ruft die Methode Where in einem IQueryable-Objekt auf, und der Filter wird auf dem Server verarbeitet. In einigen Szenarios ruft die App möglicherweise die Methode Where als Erweiterungsmethode für eine Auflistung im Speicher auf. Angenommen, _context.Students wird beispielsweise von EF CoreDbSet in eine Repositorymethode geändert, die eine IEnumerable-Sammlung zurückgibt. Das Ergebnis wäre normalerweise identisch, aber in einigen Fällen kann es unterschiedlich ausfallen.

Bei der .NET Framework-Implementierung von Contains wird beispielsweise standardmäßig ein Vergleich unter Beachtung der Groß-/Kleinschreibung vorgenommen. In SQL Server wird die Unterscheidung von Groß-/Kleinschreibung bei Contains durch die Einstellung für die Sortierung der SQL Server-Instanz bestimmt. Bei SQL Server wird die Groß-/Kleinschreibung standardmäßig nicht beachtet. ToUpper könnte aufgerufen werden, wenn die Groß-/Kleinschreibung bei der Durchführung des Tests explizit nicht beachtet werden soll:

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

Der vorangehende Code würde sicherstellen, dass die Groß-/Kleinschreibung bei Ergebnissen nicht beachtet wird, wenn der Code so geändert wird, dass er IEnumerable verwendet. Wenn Contains in einer IEnumerable-Sammlung verwendet wird, wird die .NET Core-Implementierung verwendet. Wenn Contains in einem IQueryable-Objekt aufgerufen wird, wird die Datenbankimplementierung verwendet. Die Rückgabe eines Objekts vom Typ IEnumerable aus einem Repository kann signifikante Leistungseinbußen mit sich bringen:

  1. Alle Zeilen werden vom Datenbankserver zurückgegeben.
  2. Der Filter wird auf alle zurückgegebenen Zeilen in der Anwendung angewendet.

Beim Aufrufen von ToUpper kommt es zu Leistungseinbußen. Der ToUpper-Code in der WHERE-Klausel der TSQL SELECT-Anweisung eine Funktion hinzu. Durch die hinzugefügte Funktion wird verhindert, dass der Optimierer einen Index verwendet. Da in SQL die Groß-/Kleinschreibung beachtet wird, sollte ToUpper möglichst nicht aufgerufen werden, wenn dies nicht notwendig ist.

Hinzufügen eines Suchfelds zur Studentenindexseite

Fügen Sie folgenden hervorgehobenen Code zu Pages/Students/Index.cshtml hinzu, um die Schaltfläche Suchen und sortiertes Chrom zu erstellen.

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

Der vorangehende Code verwendet das <form>-Taghilfsprogramm, um das Suchtextfeld und die Schaltfläche hinzuzufügen. Das <form>-Taghilfsprogramm sendet Formulardaten standardmäßig mit einer POST-Anforderung. Mit der POST-Anforderung werden die Parameter im HTTP-Nachrichtentext und nicht in der URL übergeben. Bei Verwendung einer HTTP GET-Anforderung werden die Formulardaten als Abfragezeichenfolgen in der URL übergeben. Durch die Übergabe der Daten mit Abfragezeichenfolgen können Benutzer die URL als Lesezeichen speichern. In den W3C-Richtlinien wird die Verwendung einer GET-Anforderung empfohlen, wenn die Aktion nicht zu einem Update führt.

Testen der App:

  • Klicken Sie auf die Registerkarte Studenten, und geben Sie eine Suchzeichenfolge ein.
  • Klicken Sie auf Suchen.

Beachten Sie, dass die URL die Suchzeichenfolge enthält.

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

Wenn die Seite als Lesezeichen gespeichert wird, enthält das Lesezeichen die URL der Seite und die SearchString-Abfragezeichenfolge. Durch method="get" im form-Tag wurde die Abfragezeichenfolge generiert.

Aktuell geht der Filterwert aus dem Feld Suchen verloren, wenn ein Link zur Sortierung der Spaltenüberschrift ausgewählt wird. Das Problem des verlorenen Filterwerts wird im nächsten Abschnitt behoben.

Hinzufügen von Pagingfunktionen der Studentenindexseite

In diesem Abschnitt wird eine PaginatedList-Klasse zur Unterstützung von Paging erstellt. Die PaginatedList-Klasse verwendet Skip- und Take-Anweisungen zum Filtern von Daten auf dem Server, statt alle Zeilen der Tabelle abzurufen. Die folgende Abbildung zeigt die Pagingschaltflächen.

Students index page with paging links

Erstellen Sie im Projektordner PaginatedList.cs mit folgendem Code:

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);
        }
    }
}

Die Methode CreateAsync im vorangehenden Code übernimmt die Seitengröße und die Seitenzahl und wendet die entsprechenden Skip- und Take-Anweisungen auf das Objekt IQueryable an. Wenn ToListAsync im Objekt IQueryable aufgerufen wird, wird eine Liste zurückgegeben, die nur die angeforderte Seite enthält. Die Eigenschaften HasPreviousPage und HasNextPage dienen zum Aktivieren oder Deaktivieren der Schaltflächen Zurück und Weiter für das Paging.

Die Methode CreateAsync dient zum Erstellen des Objekts PaginatedList<T>. Ein Konstruktor kann das Objekt PaginatedList<T> nicht erstellen. Konstruktoren können keinen asynchronen Code ausführen.

Fügen Sie Pagingfunktionen zur Indexmethode hinzu

Aktualisieren Sie in Students/Index.cshtml.cs den Typ von Student von IList<Student> in PaginatedList<Student>:

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

Aktualisieren Sie Students/Index.cshtml.csOnGetAsync mit dem folgenden Code:

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);
}

Der vorangehende Code fügt den Seitenindex, die aktuelle sortOrder-Eigenschaft und die currentFilter-Eigenschaft zur Methodensignatur hinzu.

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

Alle Parameter sind NULL, wenn:

  • Die Seite über den Link Studenten aufgerufen wird.
  • Der Benutzer auf keinen Link für das Paging oder die Sortierung geklickt hat.

Wenn auf einen Paging-Link geklickt wird, enthält die Seitenindexvariable die anzuzeigende Seitenzahl.

CurrentSort stellt die Razor Page mit der aktuellen Sortierreihenfolge bereit. Die aktuelle Sortierreihenfolge muss in den Paging-Links enthalten sein, damit die Sortierreihenfolge während des Pagings beibehalten wird.

CurrentFilter stellt die Razor Page mit der aktuellen Filterzeichenfolge bereit. Der CurrentFilter-Wert:

  • Muss in den Paging-Links enthalten sein, damit die Filtereinstellungen während des Pagings beibehalten werden.
  • Muss im Textfeld wiederhergestellt werden, wenn die Seite erneut angezeigt wird.

Wenn die Suchzeichenfolge während des Pagings geändert wird, wird die Seite auf 1 zurückgesetzt. Die Seite muss auf 1 zurückgesetzt werden, da der neue Filter andere Daten anzeigen kann. Folgendes gilt, wenn ein Suchwert eingegeben und auf Senden geklickt wird:

  • Die Suchzeichenfolge wird geändert.
  • Der Parameter searchString ist nicht NULL.
if (searchString != null)
{
    pageIndex = 1;
}
else
{
    searchString = currentFilter;
}

Die Methode PaginatedList.CreateAsync konvertiert die Studentenabfrage in eine einzelne Studentenseite in einem Sammlungstyp, der das Paging unterstützt. Diese einzelne Studentenseite wird an die Razor Page übergeben.

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

Die zwei Fragezeichen in PaginatedList.CreateAsync stellen den NULL-Sammeloperator dar. Der NULL-Sammeloperator definiert einen Standardwert für einen auf NULL festlegbaren Typ. Der Ausdruck (pageIndex ?? 1) bedeutet, dass der Wert von pageIndex zurückgegeben wird, wenn dieser einen Wert aufweist. Wenn der pageIndex keinen Wert aufweist, wird 1 zurückgegeben.

Aktualisieren Sie das Markup in Students/Index.cshtml. Die Änderungen werden hervorgehoben:

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

In den Links der Spaltenüberschriften wird die Abfragezeichenfolge verwendet, um die aktuelle Suchzeichenfolge an die Methode OnGetAsync zu übergeben, damit der Benutzer Filterergebnisse sortieren kann:

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

Die Pagingschaltflächen werden durch Taghilfsprogramme angezeigt:


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

Führen Sie die App aus, und navigieren Sie zur Studentenseite.

  • Klicken Sie in verschiedenen Sortierreihenfolgen auf die Paging-Links, um sicherzustellen, dass das Paging funktioniert.
  • Geben Sie eine Suchzeichenfolge ein und versuchen Sie, das Paging durchzuführen, um sicherzustellen, dass das Paging bei Sortier- und Filtervorgängen ordnungsgemäß funktioniert.

students index page with paging links

So erhalten Sie ein besseres Verständnis des Codes:

  • Legen Sie in Students/Index.cshtml.cs einen Breakpoint bei switch (sortOrder) fest.
  • Fügen Sie für NameSort, DateSort, CurrentSort und Model.Student.PageIndex ein Überwachungselement hinzu.
  • Legen Sie in Students/Index.cshtml einen Breakpoint bei @Html.DisplayNameFor(model => model.Student[0].LastName) fest.

Führen Sie den Debugger schrittweise aus.

Aktualisieren der Info-Seite zum Anzeigen von Studentenstatistiken

In diesem Schritt wird Pages/About.cshtml aktualisiert, um anzuzeigen, wie viele Studenten sich am jeweiligen Anmeldedatum angemeldet haben. Bei dem Update wird eine Gruppierung verwendet, und es umfasst die folgenden Schritte:

  • Das Erstellen eines Ansichtsmodells für die auf der Info-Seite verwendeten Daten.
  • Das Aktualisieren der Info-Seite, um das Ansichtsmodell zu verwenden.

Erstellen des Ansichtsmodells

Erstellen Sie im Ordner Models (Modelle) den Ordner SchoolViewModels.

Fügen Sie im Ordner SchoolViewModels die Datei EnrollmentDateGroup.cs mit folgendem Code hinzu:

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; }
    }
}

Aktualisieren des Info-Seitenmodells

Die Webvorlagen in ASP.NET Core 2.2 enthalten nicht die Seite „Info“. Erstellen Sie bei Verwenden von ASP.NET Core 2.2 die „Informationen zu Razor Page“.

Aktualisieren Sie die Datei Pages/About.cshtml.cs mit dem folgenden Code:

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();
        }
    }
}

Die LINQ-Anweisung gruppiert die Studentenentitäten nach Anmeldedatum, berechnet die Anzahl der Entitäten in jeder Gruppe und speichert die Ergebnisse in einer Sammlung von EnrollmentDateGroup-Ansichtsmodellobjekten.

Ändern der Seite „Informationen zu Razor Page“

Ersetzen Sie den Code in der Datei Pages/About.cshtml durch folgenden Code:

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

Führen Sie die App aus, und navigieren Sie zur Seite „Info“. Die Anzahl der Studenten für die jeweiligen Anmeldedatumswerte wird in einer Tabelle angezeigt.

Wenn nicht zu lösende Probleme auftreten, laden Sie die abgeschlossene Anwendung für diese Phase herunter.

About page

Zusätzliche Ressourcen

Im nächsten Tutorial verwendet die App Migrationen zum Aktualisieren des Datenmodells.