Implementace efektivního stránkování dat
od Microsoftu
Toto je krok 8 bezplatného kurzu aplikace NerdDinner , který vás provede sestavením malé, ale úplné webové aplikace pomocí ASP.NET MVC 1.
Krok 8 ukazuje, jak přidat podporu stránkování do adresy URL /Dinners tak, abychom místo zobrazení 1000 večeří najednou zobrazili jenom 10 nadcházejících večeří a umožnili koncovým uživatelům, aby se v celém seznamu vraceli a přeposílali způsobem, který je přívětivý k SEO.
Pokud používáte ASP.NET MVC 3, doporučujeme postupovat podle kurzů Začínáme S MVC 3 nebo MVC Music Store.
NerdDinner Krok 8: Podpora stránkování
Pokud bude náš web úspěšný, bude mít tisíce nadcházejících večeří. Musíme zajistit, aby naše uživatelské rozhraní zvládlo všechny tyto večeře a umožnilo uživatelům procházet je. Abychom to mohli povolit, přidáme do adresy URL /Dinners podporu stránkování, abychom místo zobrazení 1000 večeří najednou zobrazili jenom 10 nadcházejících večeří a umožnili koncovým uživatelům procházet celý seznam tak, aby se v seo přívětivém stylu zobrazovali stránky.
Index() – rekapitulace metody akce
Metoda akce Index() v rámci naší třídy DinnersController aktuálně vypadá takto:
//
// GET: /Dinners/
public ActionResult Index() {
var dinners = dinnerRepository.FindUpcomingDinners().ToList();
return View(dinners);
}
Když se na adresu URL /Dinners odešle požadavek, načte se seznam všech nadcházejících večeří a pak se zobrazí výpis všech z nich:
Principy IQueryable<T>
Iqueryable<T> je rozhraní, které bylo zavedeno s LINQ jako součást .NET 3.5. Umožňuje výkonné scénáře s odloženým spuštěním, které můžeme využít k implementaci podpory stránkování.
V naší části DinnerRepository vracíme posloupnost IQueryable<Dinner> z naší metody FindUpcomingDinners():
public class DinnerRepository {
private NerdDinnerDataContext db = new NerdDinnerDataContext();
//
// Query Methods
public IQueryable<Dinner> FindUpcomingDinners() {
return from dinner in db.Dinners
where dinner.EventDate > DateTime.Now
orderby dinner.EventDate
select dinner;
}
Objekt IQueryable<Dinner> vrácený metodou FindUpcomingDinners() zapouzdří dotaz pro načtení objektů Dinner z naší databáze pomocí LINQ to SQL. Důležité je, že se dotaz nespustí proti databázi, dokud se nepokusíme o přístup k datům v dotazu nebo dokud se nepokusíme o přístup k datům v dotazu nebo dokud nezavoláme metodu ToList(). Kód volající metodu FindUpcomingDinners() může volitelně přidat další "zřetěděné" operace nebo filtry do objektu IQueryable<Dinner> před spuštěním dotazu. LINQ to SQL je pak dostatečně inteligentní, aby při vyžádání dat spustil kombinovaný dotaz na databázi.
Abychom implementovali logiku stránkování, můžeme aktualizovat metodu akce Index() aplikace DinnersController tak, aby na vrácenou sekvenci IQueryable<Dinner> použila další operátory Skip a Take před voláním toList():
//
// GET: /Dinners/
public ActionResult Index() {
var upcomingDinners = dinnerRepository.FindUpcomingDinners();
var paginatedDinners = upcomingDinners.Skip(10).Take(20).ToList();
return View(paginatedDinners);
}
Výše uvedený kód přeskočí prvních 10 nadcházejících večeří v databázi a pak vrátí zpět 20 večeří. LINQ to SQL je dostatečně chytrá na to, aby se vytvořil optimalizovaný dotaz SQL, který tuto logiku přeskočení provádí v databázi SQL , a ne na webovém serveru. To znamená, že i když máme v databázi miliony nadcházejících večeří, v rámci tohoto požadavku se načte pouze 10 požadovaných (díky tomu bude efektivní a škálovatelný).
Přidání hodnoty "page" k adrese URL
Místo pevného kódování konkrétního rozsahu stránek budeme chtít, aby adresy URL obsahovaly parametr "page", který označuje rozsah Večeře, který uživatel požaduje.
Použití hodnoty řetězce dotazu
Následující kód ukazuje, jak můžeme aktualizovat naši metodu akce Index() tak, aby podporovala parametr řetězce dotazů, a povolit adresy URL, jako je /Dinners?page=2:
//
// GET: /Dinners/
// /Dinners?page=2
public ActionResult Index(int? page) {
const int pageSize = 10;
var upcomingDinners = dinnerRepository.FindUpcomingDinners();
var paginatedDinners = upcomingDinners.Skip((page ?? 0) * pageSize)
.Take(pageSize)
.ToList();
return View(paginatedDinners);
}
Výše uvedená metoda akce Index() má parametr s názvem "page". Parametr je deklarován jako celé číslo s možnou hodnotou null (to int? označuje). To znamená, že adresa URL /Dinners?page=2 způsobí předání hodnoty "2" jako hodnota parametru. Adresa URL /Dinners (bez hodnoty řetězce dotazu) způsobí předání hodnoty null.
Hodnotu stránky vynásobíme velikostí stránky (v tomto případě 10 řádků), abychom zjistili, kolik večeří se má přeskočit. Používáme operátor "sjednocení" (??) s hodnotou null jazyka C# , který je užitečný při práci s typy s možnou hodnotou null. Výše uvedený kód přiřadí stránce hodnotu 0, pokud má parametr stránky hodnotu null.
Použití hodnot vložených adres URL
Alternativou k použití hodnoty řetězce dotazu by bylo vložení parametru stránky do samotné vlastní adresy URL. Příklad: /Dinners/Page/2 nebo /Dinners/2. ASP.NET MVC obsahuje výkonný modul směrování adres URL, který usnadňuje podporu podobných scénářů.
Můžeme zaregistrovat vlastní pravidla směrování, která mapují jakýkoli formát příchozí adresy URL nebo adresy URL na libovolnou třídu kontroleru nebo metodu akce, kterou chceme. Stačí otevřít soubor Global.asax v rámci našeho projektu:
Pak zaregistrujte nové pravidlo mapování pomocí pomocné metody MapRoute(), jako je první volání tras. MapRoute() níže:
public void RegisterRoutes(RouteCollection routes) {
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"UpcomingDinners", // Route name
"Dinners/Page/{page}", // URL with params
new { controller = "Dinners", action = "Index" } // Param defaults
);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with params
new { controller="Home", action="Index",id="" } // Param defaults
);
}
void Application_Start() {
RegisterRoutes(RouteTable.Routes);
}
Výše zaregistrujeme nové pravidlo směrování s názvem UpcomingDinners. Označujeme, že má formát adresy URL Dinners/Page/{page}, kde {page} je hodnota parametru vložená v adrese URL. Třetí parametr metody MapRoute() označuje, že bychom měli mapovat adresy URL, které odpovídají tomuto formátu, na metodu akce Index() ve třídě DinnersController.
Můžeme použít stejný kód Index(), který jsme měli dříve ve scénáři s řetězcem dotazů – s tím rozdílem, že teď náš parametr "page" bude pocházet z adresy URL, a ne z řetězce dotazu:
//
// GET: /Dinners/
// /Dinners/Page/2
public ActionResult Index(int? page) {
const int pageSize = 10;
var upcomingDinners = dinnerRepository.FindUpcomingDinners();
var paginatedDinners = upcomingDinners.Skip((page ?? 0) * pageSize)
.Take(pageSize)
.ToList();
return View(paginatedDinners);
}
A teď, když spustíme aplikaci a napíšeme /Dinners , uvidíme prvních 10 nadcházejících večeří:
A když napíšeme /Dinners/Page/1 , uvidíme další stránku s večeřemi:
Přidání uživatelského rozhraní navigace na stránce
Posledním krokem k dokončení našeho scénáře stránkování bude implementace navigačního uživatelského rozhraní "next" a "previous" v rámci naší šablony zobrazení, aby uživatelé mohli snadno přeskočit data Večeře.
Abychom to mohli správně implementovat, musíme znát celkový počet večeří v databázi a také počet stránek dat, na které se to překládá. Pak budeme muset vypočítat, jestli je aktuálně požadovaná hodnota "page" na začátku nebo na konci dat, a odpovídajícím způsobem zobrazit nebo skrýt předchozí a další uživatelské rozhraní. Tuto logiku bychom mohli implementovat v rámci naší metody akce Index(). Případně můžeme do našeho projektu přidat pomocnou třídu, která zapouzdřuje tuto logiku lépe použitelným způsobem.
Níže je jednoduchá pomocná třída "PaginatedList", která je odvozena od třídy kolekce List<T> integrované do rozhraní .NET Framework. Implementuje opakovaně použitelnou třídu kolekce, kterou lze použít k stránkování libovolné sekvence dat IQueryable. V naší aplikaci NerdDinner budeme pracovat s výsledky IQueryable<Dinner> , ale stejně snadno se dá použít pro výsledky IQueryable<Product> nebo IQueryable<Customer> v jiných aplikačních scénářích:
public class PaginatedList<T> : List<T> {
public int PageIndex { get; private set; }
public int PageSize { get; private set; }
public int TotalCount { get; private set; }
public int TotalPages { get; private set; }
public PaginatedList(IQueryable<T> source, int pageIndex, int pageSize) {
PageIndex = pageIndex;
PageSize = pageSize;
TotalCount = source.Count();
TotalPages = (int) Math.Ceiling(TotalCount / (double)PageSize);
this.AddRange(source.Skip(PageIndex * PageSize).Take(PageSize));
}
public bool HasPreviousPage {
get {
return (PageIndex > 0);
}
}
public bool HasNextPage {
get {
return (PageIndex+1 < TotalPages);
}
}
}
Všimněte si výše, jak vypočítá a pak zveřejňuje vlastnosti, jako jsou PageIndex, PageSize, TotalCount a TotalPages. Pak také zveřejňuje dvě pomocné vlastnosti HasPreviousPage a HasNextPage, které označují, zda je stránka dat v kolekci na začátku nebo na konci původní sekvence. Výše uvedený kód způsobí, že se spustí dva dotazy SQL – první načte celkový počet objektů Dinner (nevrací objekty – místo toho provede příkaz SELECT COUNT, který vrátí celé číslo), a druhý načte pouze řádky dat, které potřebujeme z naší databáze pro aktuální stránku dat.
Pak můžeme aktualizovat naši pomocnou metodu DinnersController.Index(), aby z výsledku DinnerRepository.FindUpcomingDinners() vytvořila paginatedList<Dinner> a předala ji do naší šablony zobrazení:
//
// GET: /Dinners/
// /Dinners/Page/2
public ActionResult Index(int? page) {
const int pageSize = 10;
var upcomingDinners = dinnerRepository.FindUpcomingDinners();
var paginatedDinners = new PaginatedList<Dinner>(upcomingDinners, page ?? 0, pageSize);
return View(paginatedDinners);
}
Pak můžeme aktualizovat šablonu zobrazení \Views\Dinners\Index.aspx tak, aby dědila z ViewPage<NerdDinner.Helpers.PaginatedList<Dinner>> místo ViewPage<IEnumerable<Dinner>>, a pak přidat následující kód do dolní části šablony zobrazení, který zobrazí nebo skryje další a předchozí navigační uživatelské rozhraní:
<% if (Model.HasPreviousPage) { %>
<%= Html.RouteLink("<<<", "UpcomingDinners", new { page = (Model.PageIndex-1) }) %>
<% } %>
<% if (Model.HasNextPage) { %>
<%= Html.RouteLink(">>>", "UpcomingDinners", new { page = (Model.PageIndex + 1) }) %>
<% } %>
Všimněte si výše, jak používáme pomocnou metodu Html.RouteLink() ke generování hypertextových odkazů. Tato metoda je podobná pomocné metodě Html.ActionLink(), kterou jsme použili dříve. Rozdíl je v tom, že adresu URL generujeme pomocí pravidla směrování UpcomingDinners, které nastavíme v souboru Global.asax. Tím zajistíte, že vygenerujeme adresy URL pro metodu akce Index(), které mají formát : /Dinners/Page/{page} – kde hodnota {page} je proměnná, kterou poskytujeme výše na základě aktuálního indexu PageIndex.
A když teď aplikaci znovu spustíme, uvidíme v prohlížeči 10 večeří najednou:
V dolní části stránky máme <<< také navigační uživatelské rozhraní, >>> které nám umožňuje přeskakovat naše data dopředu a dozadu pomocí adres URL dostupných pro vyhledávací web:
Vedlejší téma: Vysvětlení důsledků IQueryable<T> |
---|
IQueryable<T> je velmi výkonná funkce, která umožňuje celou řadu zajímavých scénářů odloženého spuštění (jako jsou stránkování a dotazy založené na složení). Stejně jako u všech výkonných funkcí chcete být opatrní s tím, jak ji používáte, a ujistěte se, že není zneužita. Je důležité si uvědomit, že vrácení výsledku IQueryable<T> z úložiště umožňuje volání kódu připojit k němu zřetězený operátor metody, a tak se účastnit konečného provádění dotazu. Pokud tuto možnost nechcete poskytnout volajícímu kódu, měli byste vrátit zpět výsledky IList<T> nebo IEnumerable<T> , které obsahují výsledky již spuštěného dotazu. Ve scénářích stránkování by to vyžadovalo, abyste do volané metody úložiště nasdílili skutečnou logiku stránkování dat. V tomto scénáři můžeme aktualizovat naši vyhledávací metodu FindUpcomingDinners() tak, aby měla podpis, který buď vrátil PaginatedList: PaginatedList< Dinner> FindUpcomingDinners(int pageIndex, int pageSize) { } Nebo vrátit zpět IList<Dinner> a použít parametr "totalCount" out k vrácení celkového počtu Dinners: IList<Dinner> FindUpcomingDinners(int pageIndex, int pageSize, out int totalCount) { } |
Další krok
Pojďme se teď podívat, jak můžeme do aplikace přidat podporu ověřování a autorizace.