Kurz: Přidání řazení, filtrování a stránkování – ASP.NET MVC s EF Core

V předchozím kurzu jste implementovali sadu webových stránek pro základní operace CRUD pro entity studentů. V tomto kurzu přidáte funkce řazení, filtrování a stránkování na stránku Index studentů. Vytvoříte také stránku, která dělá jednoduché seskupení.

Následující obrázek ukazuje, jak bude stránka vypadat po dokončení. Záhlaví sloupců jsou odkazy, na které může uživatel kliknout a seřadit podle tohoto sloupce. Opakovaným kliknutím na záhlaví sloupce se přepne mezi vzestupným a sestupným pořadím řazení.

Students index page

V tomto kurzu jste:

  • Přidání odkazů pro řazení sloupců
  • Přidání vyhledávacího pole
  • Přidání stránkování do Indexu studentů
  • Přidání stránkování do metody Index
  • Přidání stránkovaných odkazů
  • Vytvoření stránky O aplikaci

Požadavky

Pokud chcete přidat řazení na stránku Index studenta, změníte metodu Index kontroleru Students a přidáte kód do zobrazení Index studenta.

Přidání funkce řazení do metody Index

Nahraďte StudentsController.csmetodu Index následujícím kódem:

public async Task<IActionResult> Index(string sortOrder)
{
    ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
    var students = from s in _context.Students
                   select s;
    switch (sortOrder)
    {
        case "name_desc":
            students = students.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            students = students.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            students = students.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            students = students.OrderBy(s => s.LastName);
            break;
    }
    return View(await students.AsNoTracking().ToListAsync());
}

Tento kód obdrží sortOrder parametr z řetězce dotazu v adrese URL. Hodnotu řetězce dotazu poskytuje ASP.NET Core MVC jako parametr pro metodu akce. Parametr bude řetězec, který je buď "Name" nebo "Date", volitelně následovaný podtržítkem a řetězcem "desc" zadejte sestupné pořadí. Výchozí pořadí řazení je vzestupné.

Při prvním požadavku na indexovou stránku neexistuje žádný řetězec dotazu. Studenti jsou zobrazeni vzestupně podle příjmení, což je výchozí nastavení, jak je stanoveno podle případu v switch příkazu. Když uživatel klikne na hypertextový odkaz záhlaví sloupce, zobrazí se v řetězci dotazu příslušná sortOrder hodnota.

ViewData Dva prvky (NameSortParm a DateSortParm) se používají v zobrazení ke konfiguraci hypertextových odkazů záhlaví sloupců s příslušnými řetězcovými hodnotami dotazu.

public async Task<IActionResult> Index(string sortOrder)
{
    ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
    var students = from s in _context.Students
                   select s;
    switch (sortOrder)
    {
        case "name_desc":
            students = students.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            students = students.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            students = students.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            students = students.OrderBy(s => s.LastName);
            break;
    }
    return View(await students.AsNoTracking().ToListAsync());
}

Jedná se o ternární příkazy. První určuje, že pokud sortOrder je parametr null nebo prázdný, nameSortParm by měl být nastaven na "name_desc". V opačném případě by měl být nastaven na prázdný řetězec. Tyto dva příkazy umožňují zobrazení nastavit hypertextové odkazy záhlaví sloupců následujícím způsobem:

Aktuální pořadí řazení Hypertextový odkaz příjmení Hypertextový odkaz na datum
Příjmení vzestupně descending ascending
Sestupné příjmení ascending ascending
Datum vzestupně ascending descending
Sestupné datum ascending ascending

Metoda používá LINQ to Entities k určení sloupce, podle který se má řadit. Kód vytvoří proměnnou IQueryable před příkazem switch, upraví ho v příkazu switch a zavolá metodu ToListAsyncswitch za příkazem. Při vytváření a úpravě IQueryable proměnných se do databáze neposílají žádné dotazy. Dotaz se nespustí, dokud objekt nepřeveďte IQueryable do kolekce voláním metody, jako ToListAsyncje . Výsledkem tohoto kódu je tedy jeden dotaz, který se nespustí, dokud příkaz return View nespustí.

Tento kód by mohl být podrobný s velkým počtem sloupců. Poslední kurz v této řadě ukazuje, jak napsat kód, který vám umožní předat název OrderBy sloupce v řetězcové proměnné.

Nahraďte kód v Views/Students/Index.cshtml, následujícím kódem pro přidání hypertextových odkazů záhlaví sloupců. Změněné čáry jsou zvýrazněné.

@model IEnumerable<ContosoUniversity.Models.Student>

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

<h2>Index</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
                <th>
                    <a asp-action="Index" asp-route-sortOrder="@ViewData["NameSortParm"]">@Html.DisplayNameFor(model => model.LastName)</a>
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.FirstMidName)
                </th>
                <th>
                    <a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]">@Html.DisplayNameFor(model => model.EnrollmentDate)</a>
                </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <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-action="Edit" asp-route-id="@item.ID">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

Tento kód používá informace ve ViewData vlastnostech k nastavení hypertextových odkazů s příslušnými hodnotami řetězce dotazu.

Spusťte aplikaci, vyberte kartu Studenti a kliknutím na záhlaví sloupců Příjmení a Datum registrace ověřte, že řazení funguje.

Students index page in name order

Pokud chcete přidat filtrování na stránku Index studentů, přidáte do zobrazení textové pole a tlačítko odeslat a v Index metodě provedete odpovídající změny. Textové pole vám umožní zadat řetězec, který se má hledat v polích křestní jméno a příjmení.

Přidání funkce filtrování do metody Index

Nahraďte StudentsController.csmetodu Index následujícím kódem (změny jsou zvýrazněny).

public async Task<IActionResult> Index(string sortOrder, string searchString)
{
    ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
    ViewData["CurrentFilter"] = searchString;

    var students = from s in _context.Students
                   select s;
    if (!String.IsNullOrEmpty(searchString))
    {
        students = students.Where(s => s.LastName.Contains(searchString)
                               || s.FirstMidName.Contains(searchString));
    }
    switch (sortOrder)
    {
        case "name_desc":
            students = students.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            students = students.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            students = students.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            students = students.OrderBy(s => s.LastName);
            break;
    }
    return View(await students.AsNoTracking().ToListAsync());
}

Do metody jste přidali searchString parametr Index . Hodnota vyhledávacího řetězce se přijímá z textového pole, které přidáte do zobrazení indexu. Přidali jste také do příkazu LINQ klauzuli where, která vybere jenom studenty, jejichž křestní jméno nebo příjmení obsahuje hledaný řetězec. Příkaz, který přidá klauzuli where, se spustí pouze v případě, že je hodnota, kterou chcete vyhledat.

Poznámka

Tady voláte metodu WhereIQueryable na objektu a filtr se zpracuje na serveru. V některých scénářích můžete metodu Where volat jako metodu rozšíření v kolekci v paměti. (Předpokládejme například, že změníte odkaz tak _context.Students , aby místo EF DbSet odkazovat na metodu úložiště, která vrací IEnumerable kolekci.) Výsledek by normálně byl stejný, ale v některých případech se může lišit.

Například implementace Contains rozhraní .NET Framework metody ve výchozím nastavení provádí porovnání s rozlišováním malých a malých písmen, ale v SQL Serveru je to určeno nastavením kolace instance SYSTÉMU SQL Server. Toto nastavení ve výchozím nastavení nerozlišuje malá a velká písmena. Metodu ToUpper můžete volat tak, aby test explicitně nerozlišil malá a velká písmena: Where(s.LastName.ToUpper>(). Contains(searchString.ToUpper()) To by zajistilo, že výsledky zůstanou stejné, pokud později změníte kód tak, aby používal úložiště, které vrací IEnumerable kolekci místo objektu IQueryable . (Když voláte metodu ContainsIEnumerable v kolekci, získáte implementaci rozhraní .NET Framework; při volání objektu IQueryable získáte implementaci zprostředkovatele databáze.) Pro toto řešení je ale penalizace výkonu. Kód ToUpper by vložil funkci do klauzule WHERE příkazu TSQL SELECT. To by zabránilo optimalizátoru v používání indexu. Vzhledem k tomu, že SQL je většinou nainstalovaný jako nerozlišující malá a velká písmena, je nejlepší se vyhnout ToUpper kódu, dokud nemigrujete do úložiště dat citlivých na malá a velká písmena.

Přidání vyhledávacího pole do zobrazení indexu studentů

Pokud Views/Student/Index.cshtmlchcete vytvořit titulek, textové pole a tlačítko Hledat , přidejte zvýrazněný kód bezprostředně před levou značku tabulky.

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

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

<table class="table">

Tento kód používá pomocnou rutinu<form> značky k přidání vyhledávacího textového pole a tlačítka. Ve výchozím nastavení <form> pomocné rutina značky odesílá data formuláře pomocí POST, což znamená, že parametry se předávají v textu zprávy HTTP, a ne v adrese URL jako řetězce dotazu. Když zadáte HTTP GET, data formuláře se předají v adrese URL jako řetězce dotazu, což umožňuje uživatelům vytvořit záložku adresy URL. Pokyny pro W3C doporučují, abyste funkci GET použili, když akce nevyústit v aktualizaci.

Spusťte aplikaci, vyberte kartu Studenti , zadejte hledaný řetězec a kliknutím na Hledat ověřte, že filtrování funguje.

Students index page with filtering

Všimněte si, že adresa URL obsahuje hledaný řetězec.

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

Pokud si tuto stránku vytvoříte záložkou, zobrazí se filtrovaný seznam při použití této záložky. Přidání method="get" do značky form je to, co způsobilo vygenerování řetězce dotazu.

Pokud v této fázi kliknete na odkaz pro řazení záhlaví sloupce, ztratíte hodnotu filtru, kterou jste zadali do vyhledávacího pole. Opravíte to v další části.

Přidání stránkování do Indexu studentů

Pokud chcete přidat stránkování na stránku Index studentů, vytvoříte PaginatedList třídu, která používá Skip a Take uvádí příkazy k filtrování dat na serveru, a ne vždy načítá všechny řádky tabulky. Pak v metodě provedete další změny Index a do zobrazení přidáte stránkovací tlačítka Index . Následující obrázek znázorňuje stránkovací tlačítka.

Students index page with paging links

Ve složce projektu vytvořte PaginatedList.csa nahraďte kód šablony následujícím kódem.

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 v tomto kódu přebírá velikost stránky a číslo stránky a použije příslušné Skip příkazy a Take příkazy na IQueryable. Když ToListAsync je volána na této stránce IQueryable, vrátí seznam obsahující pouze požadovanou stránku. Vlastnosti HasPreviousPage a HasNextPage lze je použít k povolení nebo zakázání tlačítek předchozí a další stránkování.

Metoda CreateAsync se používá místo konstruktoru k vytvoření objektu PaginatedList<T> , protože konstruktory nemůžou spouštět asynchronní kód.

Přidání stránkování do metody Index

Nahraďte StudentsController.csmetodu Index následujícím kódem.

public async Task<IActionResult> Index(
    string sortOrder,
    string currentFilter,
    string searchString,
    int? pageNumber)
{
    ViewData["CurrentSort"] = sortOrder;
    ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";

    if (searchString != null)
    {
        pageNumber = 1;
    }
    else
    {
        searchString = currentFilter;
    }

    ViewData["CurrentFilter"] = searchString;

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

    int pageSize = 3;
    return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), pageNumber ?? 1, pageSize));
}

Tento kód přidá parametr čísla stránky, aktuální parametr pořadí řazení a aktuální parametr filtru do podpisu metody.

public async Task<IActionResult> Index(
    string sortOrder,
    string currentFilter,
    string searchString,
    int? pageNumber)

Při prvním zobrazení stránky nebo pokud uživatel neklikli na stránkovací nebo seřazovat odkaz, všechny parametry budou null. Pokud kliknete na stránkovací odkaz, proměnná stránky bude obsahovat číslo stránky, které se má zobrazit.

Element ViewData s názvem CurrentSort poskytuje zobrazení s aktuálním pořadím řazení, protože to musí být součástí stránkovaných odkazů, aby pořadí řazení bylo stejné při stránkování.

Element ViewData s názvem CurrentFilter poskytuje zobrazení s aktuálním řetězcem filtru. Tato hodnota musí být součástí stránkovaných odkazů, aby se zachovalo nastavení filtru během stránkování, a při opětovném přehrání stránky se musí obnovit do textového pole.

Pokud se vyhledávací řetězec během stránkování změní, musí být stránka resetována na 1, protože nový filtr může vést k zobrazení různých dat. Hledaný řetězec se změní, když je do textového pole zadána hodnota a stisknete tlačítko Odeslat. V takovém případě searchString parametr nemá hodnotu null.

if (searchString != null)
{
    pageNumber = 1;
}
else
{
    searchString = currentFilter;
}

Na konci Index metody PaginatedList.CreateAsync metoda převede dotaz studenta na jednu stránku studentů v typu kolekce, která podporuje stránkování. Tato jedna stránka studentů se pak předá do zobrazení.

return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), pageNumber ?? 1, pageSize));

Metoda PaginatedList.CreateAsync přebírá číslo stránky. Dvě otazníky představují operátor null-coalescing. Operátor null-coalescing definuje výchozí hodnotu pro typ s možnou hodnotou null; výraz (pageNumber ?? 1) znamená, že vrátí hodnotu pageNumber , pokud má hodnotu, nebo vrátí hodnotu 1, pokud pageNumber je null.

Nahraďte Views/Students/Index.cshtmlstávající kód následujícím kódem. Změny jsou zvýrazněné.

@model PaginatedList<ContosoUniversity.Models.Student>

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

<h2>Index</h2>

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

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

<table class="table">
    <thead>
        <tr>
            <th>
                <a asp-action="Index" asp-route-sortOrder="@ViewData["NameSortParm"]" asp-route-currentFilter="@ViewData["CurrentFilter"]">Last Name</a>
            </th>
            <th>
                First Name
            </th>
            <th>
                <a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]" asp-route-currentFilter="@ViewData["CurrentFilter"]">Enrollment Date</a>
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            <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-action="Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

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

<a asp-action="Index"
   asp-route-sortOrder="@ViewData["CurrentSort"]"
   asp-route-pageNumber="@(Model.PageIndex - 1)"
   asp-route-currentFilter="@ViewData["CurrentFilter"]"
   class="btn btn-default @prevDisabled">
    Previous
</a>
<a asp-action="Index"
   asp-route-sortOrder="@ViewData["CurrentSort"]"
   asp-route-pageNumber="@(Model.PageIndex + 1)"
   asp-route-currentFilter="@ViewData["CurrentFilter"]"
   class="btn btn-default @nextDisabled">
    Next
</a>

Příkaz @model v horní části stránky určuje, že zobrazení teď získá PaginatedList<T> objekt místo objektu List<T> .

Odkazy záhlaví sloupce používají řetězec dotazu k předání aktuálního vyhledávacího řetězce kontroleru, aby uživatel mohl řadit výsledky filtru:

<a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]" asp-route-currentFilter ="@ViewData["CurrentFilter"]">Enrollment Date</a>

Stránkovací tlačítka se zobrazují pomocnými rutiny značek:

<a asp-action="Index"
   asp-route-sortOrder="@ViewData["CurrentSort"]"
   asp-route-pageNumber="@(Model.PageIndex - 1)"
   asp-route-currentFilter="@ViewData["CurrentFilter"]"
   class="btn btn-default @prevDisabled">
   Previous
</a>

Spusťte aplikaci a přejděte na stránku Studenti.

Students index page with paging links

Kliknutím na stránkovací odkazy v různých pořadích řazení se ujistěte, že stránkování funguje. Pak zadejte vyhledávací řetězec a zkuste stránkování znovu ověřit, že stránkování funguje správně i s řazením a filtrováním.

Vytvoření stránky O aplikaci

Na stránce Informace o webu Contoso University se zobrazí počet studentů zaregistrovaných pro každé datum registrace. To vyžaduje seskupování a jednoduché výpočty skupin. Uděláte to takto:

  • Vytvořte třídu modelu zobrazení pro data, která potřebujete předat do zobrazení.
  • Vytvořte v kontroleru metodu Home About.
  • Vytvořte zobrazení O aplikaci.

Vytvoření modelu zobrazení

Ve složce Modely vytvořte složku SchoolViewModels.

Do nové složky přidejte soubor EnrollmentDateGroup.cs třídy a nahraďte kód šablony následujícím kódem:

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

Home Úprava kontroleru

Do HomeController.cssouboru přidejte následující příkazy using v horní části souboru:

using Microsoft.EntityFrameworkCore;
using ContosoUniversity.Data;
using ContosoUniversity.Models.SchoolViewModels;
using Microsoft.Extensions.Logging;

Přidejte proměnnou třídy pro kontext databáze hned po otevření složené závorky pro třídu a získejte instanci kontextu z ASP.NET Core DI:

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    private readonly SchoolContext _context;

    public HomeController(ILogger<HomeController> logger, SchoolContext context)
    {
        _logger = logger;
        _context = context;
    }

Přidejte metodu About s následujícím kódem:

public async Task<ActionResult> About()
{
    IQueryable<EnrollmentDateGroup> data = 
        from student in _context.Students
        group student by student.EnrollmentDate into dateGroup
        select new EnrollmentDateGroup()
        {
            EnrollmentDate = dateGroup.Key,
            StudentCount = dateGroup.Count()
        };
    return View(await data.AsNoTracking().ToListAsync());
}

Příkaz LINQ seskupí entity studentů podle data registrace, vypočítá počet entit v každé skupině a uloží výsledky v kolekci EnrollmentDateGroup objektů modelu zobrazení.

Vytvoření zobrazení o aplikaci

Views/Home/About.cshtml Přidejte soubor s následujícím kódem:

@model IEnumerable<ContosoUniversity.Models.SchoolViewModels.EnrollmentDateGroup>

@{
    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)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.EnrollmentDate)
            </td>
            <td>
                @item.StudentCount
            </td>
        </tr>
    }
</table>

Spusťte aplikaci a přejděte na stránku O aplikaci. Počet studentů pro každé datum registrace se zobrazí v tabulce.

Získání kódu

Stáhněte nebo zobrazte dokončenou aplikaci.

Další kroky

V tomto kurzu jste:

  • Přidání odkazů řazení sloupců
  • Přidání vyhledávacího pole
  • Přidání stránkování do indexu studentů
  • Přidání stránkování do metody Index
  • Přidání stránkovaných odkazů
  • Vytvoření stránky O aplikaci

V dalším kurzu se dozvíte, jak řešit změny datového modelu pomocí migrací.