Część 3, Razor strony w EF Core ASP.NET Core — sortowanie, filtrowanie, stronicowanie

Przez Tom Dykstra, Jeremy Likness i Jon P Smith

Aplikacja internetowa Contoso University pokazuje, jak tworzyć Razor aplikacje internetowe stron przy użyciu programu EF Core Visual Studio. Aby uzyskać informacje na temat serii samouczków, zobacz pierwszy samouczek.

Jeśli napotkasz problemy, których nie możesz rozwiązać, pobierz ukończoną aplikację i porównaj ten kod z utworzonymi elementami, wykonując czynności opisane w samouczku.

Ten samouczek dodaje funkcje sortowania, filtrowania i stronicowania na stronach Uczniowie.

Poniższa ilustracja przedstawia ukończoną stronę. Nagłówki kolumn można klikać, aby posortować kolumnę. Kliknij nagłówek kolumny wielokrotnie, aby przełączać się między kolejnością sortowania rosnącego i malejącego.

Students index page

Dodawanie sortowania

Zastąp kod w Pages/Students/Index.cshtml.cs pliku poniższym kodem, aby dodać sortowanie.

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

Powyższy kod:

  • Wymaga dodania using System;elementu .
  • Dodaje właściwości zawierające parametry sortowania.
  • Zmienia nazwę Student właściwości na Students.
  • Zastępuje kod w metodzie OnGetAsync .

Metoda OnGetAsync odbiera sortOrder parametr z ciągu zapytania w adresie URL. Adres URL i ciąg zapytania są generowane przez pomocnika tagów zakotwiczenia.

Parametr sortOrder ma wartość Name lub Date. Parametr sortOrder jest opcjonalny, a następnie określa _desc kolejność malejącą. Domyślna kolejność sortowania jest rosnąca.

Gdy strona Indeks jest żądana z linku Uczniowie , nie ma ciągu zapytania. Uczniowie są wyświetlani w kolejności rosnącej według nazwiska. Kolejność rosnąca według nazwiska jest default w instrukcji switch . Gdy użytkownik kliknie link nagłówka kolumny, odpowiednia sortOrder wartość zostanie podana w wartości ciągu zapytania.

NameSort i DateSort są używane przez Razor stronę do konfigurowania hiperlinków nagłówków kolumn z odpowiednimi wartościami ciągu zapytania:

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

Kod używa operatora warunkowego języka C# ?:. Operator ?: jest trójargumentowym operatorem, przyjmuje trzy operandy. Pierwszy wiersz określa, że gdy sortOrder wartość null lub jest pusta, NameSort jest ustawiona na name_descwartość . Jeśli sortOrder wartość nie ma wartości null lub jest pusta, NameSort jest ustawiona na pusty ciąg.

Te dwie instrukcje umożliwiają stronie ustawienie hiperlinków nagłówka kolumny w następujący sposób:

Bieżąca kolejność sortowania Hiperłącze nazwiska Hiperłącze daty
Nazwisko rosnąco malejąco ascending
Nazwisko malejące ascending ascending
Data rosnąca ascending malejąco
Data malejąco ascending ascending

Metoda używa linQ to Entities, aby określić kolumnę do sortowania. Kod inicjuje element IQueryable<Student> przed instrukcją switch i modyfikuje go w instrukcji 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();

Po utworzeniu lub zmodyfikowaniu zapytania IQueryable nie zostanie wysłane do bazy danych. Zapytanie nie jest wykonywane, dopóki IQueryable obiekt nie zostanie przekonwertowany na kolekcję. IQueryable są konwertowane na kolekcję przez wywołanie metody, takiej jak ToListAsync. IQueryable W związku z tym kod powoduje utworzenie pojedynczego zapytania, które nie zostanie wykonane, dopóki nie zostanie wykonana następująca instrukcja:

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

OnGetAsync może uzyskać pełne informacje o dużej liczbie kolumn sortowalnych. Aby uzyskać informacje na temat alternatywnego sposobu kodowania tej funkcji, zobacz Używanie dynamicznego LINQ w celu uproszczenia kodu w wersji MVC tej serii samouczków.

Zastąp kod w pliku Students/Index.cshtml, następującym kodem. Zmiany są wyróżnione.

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

Powyższy kod:

  • Dodaje hiperlinki do LastName nagłówków kolumn i EnrollmentDate .
  • Używa informacji w pliku NameSort i DateSort do konfigurowania hiperlinków z bieżącymi wartościami kolejności sortowania.
  • Zmienia nagłówek strony z Indeksu na Uczniów.
  • Zmiany Model.Student w pliku Model.Students.

Aby sprawdzić, czy sortowanie działa:

  • Uruchom aplikację i wybierz kartę Uczniowie .
  • Kliknij nagłówki kolumn.

Dodawanie filtrowania

Aby dodać filtrowanie do strony Indeks uczniów:

  • Pole tekstowe i przycisk przesyłania są dodawane do strony Razor . Pole tekstowe dostarcza ciąg wyszukiwania na pierwszym lub nazwisko.
  • Model strony jest aktualizowany w celu użycia wartości pola tekstowego.

Aktualizowanie metody OnGetAsync

Zastąp kod w Students/Index.cshtml.cs pliku następującym kodem, aby dodać filtrowanie:

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

Powyższy kod:

  • searchString Dodaje parametr do OnGetAsync metody i zapisuje wartość parametru CurrentFilter we właściwości . Wartość ciągu wyszukiwania jest odbierana z pola tekstowego dodanego w następnej sekcji.
  • Dodaje do instrukcji LINQ klauzulę Where . Klauzula Where wybiera tylko uczniów, których imię lub nazwisko zawiera ciąg wyszukiwania. Instrukcja LINQ jest wykonywana tylko wtedy, gdy istnieje wartość do wyszukania.

IQueryable a IEnumerable

Kod wywołuje metodę WhereIQueryable w obiekcie, a filtr jest przetwarzany na serwerze. W niektórych scenariuszach aplikacja może wywoływać Where metodę jako metodę rozszerzenia w kolekcji w pamięci. Załóżmy na przykład, że _context.Students zmiany z EF CoreDbSet metody repozytorium, która zwraca IEnumerable kolekcję. Wynik zwykle będzie taki sam, ale w niektórych przypadkach może być inny.

Na przykład implementacja Contains programu .NET Framework domyślnie wykonuje porównanie z uwzględnieniem wielkości liter. W programie SQL Server Contains wielkość liter jest określana przez ustawienie sortowania wystąpienia programu SQL Server. Program SQL Server domyślnie nie uwzględnia wielkości liter. Funkcja SQLite domyślnie uwzględnia wielkość liter. ToUpper można wywołać, aby jawnie bez uwzględniania wielkości liter w teście:

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

Powyższy kod gwarantuje, że filtr jest niewrażliwy na wielkość liter, nawet jeśli Where metoda jest wywoływana w elemecie IEnumerable SQLite lub jest uruchamiana.

Gdy Contains jest wywoływana IEnumerable w kolekcji, jest używana implementacja platformy .NET Core. Po Contains wywołaniu IQueryable obiektu jest używana implementacja bazy danych.

Wywoływanie Contains elementu jest IQueryable zwykle preferowane ze względu na wydajność. W programie IQueryablefiltrowanie odbywa się przez serwer bazy danych. IEnumerable Jeśli element zostanie utworzony jako pierwszy, wszystkie wiersze muszą zostać zwrócone z serwera bazy danych.

Istnieje kara za wykonanie wywołania metody ToUpper. Kod ToUpper dodaje funkcję w klauzuli WHERE instrukcji TSQL SELECT. Dodano funkcję uniemożliwia optymalizatorowi korzystanie z indeksu. Biorąc pod uwagę, że język SQL jest zainstalowany jako bez uwzględniania wielkości liter, najlepiej jest unikać ToUpper wywołania, gdy nie jest to konieczne.

Aby uzyskać więcej informacji, zobacz How to use case-insensitive query with Sqlite provider (Jak używać zapytania bez uwzględniania wielkości liter w dostawcy sqlite).

Razor Aktualizowanie strony

Zastąp kod w pliku , Pages/Students/Index.cshtml aby dodać przycisk Wyszukaj.

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

Powyższy kod używa pomocnika tagów do dodania pola tekstowego <form>i przycisku wyszukiwania. Domyślnie pomocnik tagu <form> przesyła dane formularza za pomocą wpisu POST. W przypadku funkcji POST parametry są przekazywane w treści komunikatu HTTP, a nie w adresie URL. Gdy jest używany protokół HTTP GET, dane formularza są przekazywane w adresie URL jako ciągi zapytania. Przekazywanie danych za pomocą ciągów zapytania umożliwia użytkownikom tworzenie zakładek adresu URL. Wytyczne dotyczące W3C zalecają użycie polecenia GET, gdy akcja nie spowoduje aktualizacji.

Przetestuj aplikację:

  • Wybierz kartę Uczniowie i wprowadź ciąg wyszukiwania. Jeśli używasz biblioteki SQLite, filtr nie uwzględnia wielkości liter tylko wtedy, gdy zaimplementowano opcjonalny ToUpper kod pokazany wcześniej.

  • Wybierz pozycję Wyszukaj.

Zwróć uwagę, że adres URL zawiera ciąg wyszukiwania. Przykład:

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

Jeśli strona jest oznaczona zakładką, zakładka zawiera adres URL strony i SearchString ciąg zapytania. Element method="get" w tagu form jest przyczyną wygenerowania ciągu zapytania.

Obecnie po wybraniu linku sortowania nagłówka kolumny wartość filtru z pola Wyszukiwania zostanie utracona. Utracona wartość filtru jest stała w następnej sekcji.

Dodawanie stronicowania

W tej sekcji jest tworzona PaginatedList klasa do obsługi stronicowania. Klasa PaginatedList używa Skip instrukcji i Take do filtrowania danych na serwerze zamiast pobierania wszystkich wierszy tabeli. Na poniższej ilustracji przedstawiono przyciski stronicowania.

Students index page with paging links

Tworzenie klasy PaginatedList

W folderze projektu utwórz PaginatedList.cs za pomocą następującego kodu:

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

Metoda CreateAsync w poprzednim kodzie przyjmuje rozmiar strony i numer strony oraz stosuje odpowiednie Skip instrukcje i Take do .IQueryable Gdy ToListAsync element jest wywoływany w obiekcie IQueryable, zwraca listę zawierającą tylko żądaną stronę. Właściwości HasPreviousPage i HasNextPage służą do włączania lub wyłączania przycisków Wstecz i Dalej stronicowania.

Metoda CreateAsync jest używana do utworzenia elementu PaginatedList<T>. Konstruktor nie może utworzyć PaginatedList<T> obiektu; konstruktory nie mogą uruchamiać kodu asynchronicznego.

Dodawanie rozmiaru strony do konfiguracji

Dodaj PageSize do appsettings.jsonpliku konfiguracji :

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

Dodawanie stronicowania do modelu IndexModel

Zastąp kod w pliku , Students/Index.cshtml.cs aby dodać stronicowanie.

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

Powyższy kod:

  • Zmienia typ Students właściwości z IList<Student> na PaginatedList<Student>.
  • Dodaje indeks strony, bieżący sortOrderelement i element currentFilter do OnGetAsync podpisu metody.
  • Zapisuje kolejność sortowania CurrentSort we właściwości .
  • Resetuje indeks strony do wartości 1, gdy istnieje nowy ciąg wyszukiwania.
  • Używa klasy do uzyskiwania PaginatedList jednostek Student.
  • Ustawia pageSize wartość 3 z konfiguracji, 4, jeśli konfiguracja nie powiedzie się.

Wszystkie odbierane OnGetAsync parametry mają wartość null, gdy:

  • Strona jest wywoływana z linku Uczniowie .
  • Użytkownik nie kliknął łącza stronicowania ani sortowania.

Po kliknięciu linku stronicowania zmienna indeksu strony zawiera numer strony do wyświetlenia.

Właściwość CurrentSort udostępnia stronę Razor z bieżącą kolejnością sortowania. Bieżąca kolejność sortowania musi być uwzględniona w linkach stronicowania, aby zachować kolejność sortowania podczas stronicowania.

Właściwość CurrentFilter udostępnia stronę Razor z bieżącym ciągiem filtru. Wartość CurrentFilter :

  • Aby zachować ustawienia filtru podczas stronicowania, należy dołączyć do linków stronicowania.
  • Należy przywrócić do pola tekstowego po ponownym uruchomieniu strony.

Jeśli ciąg wyszukiwania zostanie zmieniony podczas stronicowania, strona zostanie zresetowana do wartości 1. Strona musi zostać zresetowana do wartości 1, ponieważ nowy filtr może spowodować wyświetlenie różnych danych. Po wprowadzeniu wartości wyszukiwania i wybraniu opcji Prześlij :

  • Ciąg wyszukiwania jest zmieniany.
  • Parametr searchString nie ma wartości null.

Metoda PaginatedList.CreateAsync konwertuje zapytanie ucznia na jedną stronę uczniów w typie kolekcji, który obsługuje stronicowanie. Ta pojedyncza strona uczniów jest przekazywana Razor do strony.

Dwa znaki zapytania po pageIndex wywołaniu PaginatedList.CreateAsync reprezentują operator łączenia wartości null. Operator łączenia wartości null definiuje wartość domyślną dla typu dopuszczającego wartość null. Wyrażenie pageIndex ?? 1 zwraca wartość pageIndex , jeśli ma wartość, w przeciwnym razie zwraca wartość 1.

Zamień kod w pliku Students/Index.cshtml na następujący kod. Zmiany są wyróżnione:

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

Linki nagłówka kolumny używają ciągu zapytania, aby przekazać bieżący ciąg wyszukiwania do OnGetAsync metody :

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

Przyciski stronicowania są wyświetlane przez pomocników tagów:


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

Uruchom aplikację i przejdź do strony uczniowie.

  • Aby upewnić się, że stronicowanie działa, kliknij łącza stronicowania w różnych kolejności sortowania.
  • Aby sprawdzić, czy stronicowanie działa prawidłowo w przypadku sortowania i filtrowania, wprowadź ciąg wyszukiwania i spróbuj stronicować.

students index page with paging links

Grupowanie

W tej sekcji zostanie utworzona strona zawierająca About liczbę uczniów zarejestrowanych dla każdej daty rejestracji. Aktualizacja używa grupowania i obejmuje następujące kroki:

  • Utwórz model widoku dla danych używanych About przez stronę.
  • Zaktualizuj stronę, About aby korzystała z modelu widoku.

Tworzenie modelu widoku

Utwórz folder Models/SchoolViewModels.

Utwórz SchoolViewModels/EnrollmentDateGroup.cs za pomocą następującego kodu:

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

Razor Tworzenie strony

Pages/About.cshtml Utwórz plik z następującym kodem:

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

Tworzenie modelu strony

Pages/About.cshtml.cs Zaktualizuj plik przy użyciu następującego kodu:

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

Instrukcja LINQ grupuje jednostki uczniów według daty rejestracji, oblicza liczbę jednostek w każdej grupie i przechowuje wyniki w kolekcji EnrollmentDateGroup obiektów modelu widoku.

Uruchom aplikację i przejdź do strony Informacje. Liczba uczniów dla każdej daty rejestracji jest wyświetlana w tabeli.

About page

Następne kroki

W następnym samouczku aplikacja używa migracji w celu zaktualizowania modelu danych.

W tym samouczku dodano funkcje sortowania, filtrowania, grupowania i stronicowania.

Poniższa ilustracja przedstawia ukończoną stronę. Nagłówki kolumn można klikać, aby posortować kolumnę. Kliknięcie nagłówka kolumny wielokrotnie przełącza się między kolejnością sortowania rosnącego i malejącego.

Students index page

Jeśli napotkasz problemy, których nie możesz rozwiązać, pobierz ukończoną aplikację.

Dodawanie sortowania do strony Indeks

Dodaj ciągi do elementu Students/Index.cshtml.csPageModel , aby zawierały parametry sortowania:

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

Zaktualizuj element za Students/Index.cshtml.csOnGetAsync pomocą następującego kodu:

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

Powyższy kod odbiera sortOrder parametr z ciągu zapytania w adresie URL. Adres URL (w tym ciąg zapytania) jest generowany przez pomocnika tagów zakotwiczenia

Parametr sortOrder ma wartość "Name" lub "Date". Parametr sortOrder jest opcjonalny, po którym następuje "_desc", aby określić kolejność malejącą. Domyślna kolejność sortowania jest rosnąca.

Gdy strona Indeks jest żądana z linku Uczniowie , nie ma ciągu zapytania. Uczniowie są wyświetlani w kolejności rosnącej według nazwiska. Kolejność rosnąca według nazwiska to domyślna (przypadek rezerwowy) w instrukcji switch . Gdy użytkownik kliknie link nagłówka kolumny, odpowiednia sortOrder wartość zostanie podana w wartości ciągu zapytania.

NameSort i DateSort są używane przez Razor stronę do konfigurowania hiperlinków nagłówków kolumn z odpowiednimi wartościami ciągu zapytania:

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

Poniższy kod zawiera warunkowy operator języka C#?:

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

Pierwszy wiersz określa, NameSort że w przypadku sortOrder wartości null lub pustej jest ustawiona wartość "name_desc". Jeśli sortOrder wartość nie ma wartości null lub jest pusta, NameSort jest ustawiona na pusty ciąg.

Jest ?: operator również znany jako operatorternary.

Te dwie instrukcje umożliwiają stronie ustawienie hiperlinków nagłówka kolumny w następujący sposób:

Bieżąca kolejność sortowania Hiperłącze nazwiska Hiperłącze daty
Nazwisko rosnąco malejąco ascending
Nazwisko malejące ascending ascending
Data rosnąca ascending malejąco
Data malejąco ascending ascending

Metoda używa linQ to Entities, aby określić kolumnę do sortowania. Kod inicjuje element IQueryable<Student> przed instrukcją switch i modyfikuje go w instrukcji 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();
}

Po utworzeniu lub zmodyfikowaniu zapytaniaIQueryable nie zostanie wysłane do bazy danych. Zapytanie nie jest wykonywane, dopóki IQueryable obiekt nie zostanie przekonwertowany na kolekcję. IQueryable są konwertowane na kolekcję przez wywołanie metody, takiej jak ToListAsync. IQueryable W związku z tym kod powoduje utworzenie pojedynczego zapytania, które nie zostanie wykonane, dopóki nie zostanie wykonana następująca instrukcja:

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

OnGetAsync może uzyskać pełne informacje o dużej liczbie kolumn sortowalnych.

Zastąp kod w pliku Students/Index.cshtmlnastępującym wyróżnionym kodem:

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

Powyższy kod:

  • Dodaje hiperlinki do LastName nagłówków kolumn i EnrollmentDate .
  • Używa informacji w pliku NameSort i DateSort do konfigurowania hiperlinków z bieżącymi wartościami kolejności sortowania.

Aby sprawdzić, czy sortowanie działa:

  • Uruchom aplikację i wybierz kartę Uczniowie .
  • Kliknij pozycję Nazwisko.
  • Kliknij pozycję Data rejestracji.

Aby lepiej zrozumieć kod:

  • W Students/Index.cshtml.csprogramie ustaw punkt przerwania na .switch (sortOrder)
  • Dodaj zegarek dla NameSort i DateSort.
  • W Students/Index.cshtmlprogramie ustaw punkt przerwania na .@Html.DisplayNameFor(model => model.Student[0].LastName)

Wykonaj kroki debugera.

Dodawanie pola wyszukiwania do strony Indeks uczniów

Aby dodać filtrowanie do strony Indeks uczniów:

  • Pole tekstowe i przycisk przesyłania są dodawane do strony Razor . Pole tekstowe dostarcza ciąg wyszukiwania na pierwszym lub nazwisko.
  • Model strony jest aktualizowany w celu użycia wartości pola tekstowego.

Dodawanie funkcji filtrowania do metody Index

Zaktualizuj element za Students/Index.cshtml.csOnGetAsync pomocą następującego kodu:

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

Powyższy kod:

  • searchString Dodaje parametr do OnGetAsync metody . Wartość ciągu wyszukiwania jest odbierana z pola tekstowego dodanego w następnej sekcji.
  • Dodano do instrukcji LINQ klauzulę Where . Klauzula Where wybiera tylko uczniów, których imię lub nazwisko zawiera ciąg wyszukiwania. Instrukcja LINQ jest wykonywana tylko wtedy, gdy istnieje wartość do wyszukania.

Uwaga: Powyższy kod wywołuje Where metodę IQueryable na obiekcie, a filtr jest przetwarzany na serwerze. W niektórych scenariuszach aplikacja może wywoływać Where metodę jako metodę rozszerzenia w kolekcji w pamięci. Załóżmy na przykład, że _context.Students zmiany z EF CoreDbSet metody repozytorium, która zwraca IEnumerable kolekcję. Wynik zwykle będzie taki sam, ale w niektórych przypadkach może być inny.

Na przykład implementacja Contains programu .NET Framework domyślnie wykonuje porównanie z uwzględnieniem wielkości liter. W programie SQL Server Contains wielkość liter jest określana przez ustawienie sortowania wystąpienia programu SQL Server. Program SQL Server domyślnie nie uwzględnia wielkości liter. ToUpper można wywołać, aby jawnie bez uwzględniania wielkości liter w teście:

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

Powyższy kod gwarantuje, że wyniki są niewrażliwe na wielkość liter, jeśli kod zmieni się w celu użycia elementu IEnumerable. Gdy Contains jest wywoływana IEnumerable w kolekcji, jest używana implementacja platformy .NET Core. Po Contains wywołaniu IQueryable obiektu jest używana implementacja bazy danych. Zwracanie elementu IEnumerable z repozytorium może mieć znaczącą karę za wydajność:

  1. Wszystkie wiersze są zwracane z serwera bazy danych.
  2. Filtr jest stosowany do wszystkich zwracanych wierszy w aplikacji.

Istnieje kara za wykonanie wywołania metody ToUpper. Kod ToUpper dodaje funkcję w klauzuli WHERE instrukcji TSQL SELECT. Dodano funkcję uniemożliwia optymalizatorowi korzystanie z indeksu. Biorąc pod uwagę, że język SQL jest zainstalowany jako bez uwzględniania wielkości liter, najlepiej jest unikać ToUpper wywołania, gdy nie jest to konieczne.

Dodawanie pola wyszukiwania do strony Indeks ucznia

W Pages/Students/Index.cshtmlpliku dodaj następujący wyróżniony kod, aby utworzyć przycisk Wyszukaj i rozdzielony chrome.

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

Powyższy kod używa pomocnika tagów do dodania pola tekstowego <form>i przycisku wyszukiwania. Domyślnie pomocnik tagu <form> przesyła dane formularza za pomocą wpisu POST. W przypadku funkcji POST parametry są przekazywane w treści komunikatu HTTP, a nie w adresie URL. Gdy jest używany protokół HTTP GET, dane formularza są przekazywane w adresie URL jako ciągi zapytania. Przekazywanie danych za pomocą ciągów zapytania umożliwia użytkownikom tworzenie zakładek adresu URL. Wytyczne dotyczące W3C zalecają użycie polecenia GET, gdy akcja nie spowoduje aktualizacji.

Przetestuj aplikację:

  • Wybierz kartę Uczniowie i wprowadź ciąg wyszukiwania.
  • Wybierz pozycję Wyszukaj.

Zwróć uwagę, że adres URL zawiera ciąg wyszukiwania.

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

Jeśli strona jest oznaczona zakładką, zakładka zawiera adres URL strony i SearchString ciąg zapytania. Element method="get" w tagu form jest przyczyną wygenerowania ciągu zapytania.

Obecnie po wybraniu linku sortowania nagłówka kolumny wartość filtru z pola Wyszukiwania zostanie utracona. Utracona wartość filtru jest stała w następnej sekcji.

Dodawanie funkcji stronicowania do strony Indeks uczniów

W tej sekcji jest tworzona PaginatedList klasa do obsługi stronicowania. Klasa PaginatedList używa Skip instrukcji i Take do filtrowania danych na serwerze zamiast pobierania wszystkich wierszy tabeli. Na poniższej ilustracji przedstawiono przyciski stronicowania.

Students index page with paging links

W folderze projektu utwórz PaginatedList.cs za pomocą następującego kodu:

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

Metoda CreateAsync w poprzednim kodzie przyjmuje rozmiar strony i numer strony oraz stosuje odpowiednie Skip instrukcje i Take do .IQueryable Gdy ToListAsync element jest wywoływany w obiekcie IQueryable, zwraca listę zawierającą tylko żądaną stronę. Właściwości HasPreviousPage i HasNextPage służą do włączania lub wyłączania przycisków Wstecz i Dalej stronicowania.

Metoda CreateAsync jest używana do utworzenia elementu PaginatedList<T>. Konstruktor nie może utworzyć PaginatedList<T> obiektu, konstruktory nie mogą uruchamiać kodu asynchronicznego.

Dodawanie funkcji stronicowania do metody Index

W Students/Index.cshtml.cspliku zaktualizuj typ parametru Student od IList<Student> do PaginatedList<Student>:

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

Zaktualizuj element za Students/Index.cshtml.csOnGetAsync pomocą następującego kodu:

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

Powyższy kod dodaje indeks strony, bieżący sortOrderelement i parametr currentFilter do podpisu metody.

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

Wszystkie parametry mają wartość null, gdy:

  • Strona jest wywoływana z linku Uczniowie .
  • Użytkownik nie kliknął łącza stronicowania ani sortowania.

Po kliknięciu linku stronicowania zmienna indeksu strony zawiera numer strony do wyświetlenia.

CurrentSort udostępnia stronę Razor z bieżącą kolejnością sortowania. Bieżąca kolejność sortowania musi być uwzględniona w linkach stronicowania, aby zachować kolejność sortowania podczas stronicowania.

CurrentFilter Udostępnia stronę Razor z bieżącym ciągiem filtru. Wartość CurrentFilter :

  • Aby zachować ustawienia filtru podczas stronicowania, należy dołączyć do linków stronicowania.
  • Należy przywrócić do pola tekstowego po ponownym uruchomieniu strony.

Jeśli ciąg wyszukiwania zostanie zmieniony podczas stronicowania, strona zostanie zresetowana do wartości 1. Strona musi zostać zresetowana do wartości 1, ponieważ nowy filtr może spowodować wyświetlenie różnych danych. Po wprowadzeniu wartości wyszukiwania i wybraniu opcji Prześlij :

  • Ciąg wyszukiwania jest zmieniany.
  • Parametr searchString nie ma wartości null.
if (searchString != null)
{
    pageIndex = 1;
}
else
{
    searchString = currentFilter;
}

Metoda PaginatedList.CreateAsync konwertuje zapytanie ucznia na jedną stronę uczniów w typie kolekcji, który obsługuje stronicowanie. Ta pojedyncza strona uczniów jest przekazywana Razor do strony.

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

Dwa znaki zapytania w obiekcie PaginatedList.CreateAsync reprezentują operator łączenia wartości null. Operator łączenia wartości null definiuje wartość domyślną dla typu dopuszczającego wartość null. Wyrażenie (pageIndex ?? 1) oznacza zwrócenie wartości , pageIndex jeśli ma wartość. Jeśli pageIndex nie ma wartości, zwróć wartość 1.

Zaktualizuj znaczniki w pliku Students/Index.cshtml. Zmiany są wyróżnione:

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

Linki nagłówka kolumny używają ciągu zapytania, aby przekazać bieżący ciąg wyszukiwania do OnGetAsync metody, aby użytkownik mógł sortować wyniki filtru:

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

Przyciski stronicowania są wyświetlane przez pomocników tagów:


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

Uruchom aplikację i przejdź do strony uczniowie.

  • Aby upewnić się, że stronicowanie działa, kliknij łącza stronicowania w różnych kolejności sortowania.
  • Aby sprawdzić, czy stronicowanie działa prawidłowo w przypadku sortowania i filtrowania, wprowadź ciąg wyszukiwania i spróbuj stronicować.

students index page with paging links

Aby lepiej zrozumieć kod:

  • W Students/Index.cshtml.csprogramie ustaw punkt przerwania na .switch (sortOrder)
  • Dodaj zegarek dla , NameSort, CurrentSortDateSorti Model.Student.PageIndex.
  • W Students/Index.cshtmlprogramie ustaw punkt przerwania na .@Html.DisplayNameFor(model => model.Student[0].LastName)

Wykonaj kroki debugera.

Aktualizowanie strony Informacje w celu wyświetlenia statystyk uczniów

W tym kroku zostanie zaktualizowany, Pages/About.cshtml aby wyświetlić liczbę uczniów zarejestrowanych dla każdej daty rejestracji. Aktualizacja używa grupowania i obejmuje następujące kroki:

  • Utwórz model widoku dla danych używanych przez stronę informacje .
  • Zaktualizuj stronę Informacje, aby użyć modelu widoku.

Tworzenie modelu widoku

Utwórz folder SchoolViewModels w folderze Models.

W folderze SchoolViewModels dodaj element EnrollmentDateGroup.cs z następującym kodem:

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

Aktualizowanie modelu strony Informacje

Szablony internetowe w programie ASP.NET Core 2.2 nie zawierają strony Informacje. Jeśli używasz ASP.NET Core 2.2, utwórz stronę informacje Razor .

Pages/About.cshtml.cs Zaktualizuj plik przy użyciu następującego kodu:

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

Instrukcja LINQ grupuje jednostki uczniów według daty rejestracji, oblicza liczbę jednostek w każdej grupie i przechowuje wyniki w kolekcji EnrollmentDateGroup obiektów modelu widoku.

Modyfikowanie strony Informacje Razor

Zastąp kod w Pages/About.cshtml pliku następującym kodem:

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

Uruchom aplikację i przejdź do strony Informacje. Liczba uczniów dla każdej daty rejestracji jest wyświetlana w tabeli.

Jeśli napotkasz problemy, których nie możesz rozwiązać, pobierz ukończoną aplikację dla tego etapu.

About page

Dodatkowe zasoby

W następnym samouczku aplikacja używa migracji w celu zaktualizowania modelu danych.