Implementowanie wydajnego stronicowania danych

autor: Microsoft

Pobierz plik PDF

Jest to krok 8 bezpłatnego samouczka aplikacji "NerdDinner" , który zawiera instrukcje dotyczące tworzenia małej, ale kompletnej aplikacji internetowej przy użyciu ASP.NET MVC 1.

Krok 8 pokazuje, jak dodać obsługę stronicowania do naszego adresu URL /Dinners, aby zamiast wyświetlać 1000 kolacji jednocześnie, będziemy wyświetlać tylko 10 nadchodzących kolacji naraz - i umożliwić użytkownikom końcowym stronicowanie z powrotem i do przodu przez całą listę w przyjazny dla seo sposób.

Jeśli używasz ASP.NET MVC 3, zalecamy skorzystanie z samouczków Wprowadzenie With MVC 3 lub MVC Music Store.

NerdDinner — krok 8. Obsługa stronicowania

Jeśli nasza strona powiedzie się, będzie miała tysiące nadchodzących kolacji. Musimy upewnić się, że nasz interfejs użytkownika jest skalowany w celu obsługi wszystkich tych kolacji i umożliwia użytkownikom ich przeglądanie. Aby to umożliwić, dodamy obsługę stronicowania do naszego adresu URL /Dinners , aby zamiast wyświetlać 1000 kolacji jednocześnie, będziemy wyświetlać tylko 10 nadchodzących kolacji naraz - i umożliwić użytkownikom końcowym stronicowanie z powrotem i do przodu przez całą listę w przyjazny sposób seo.

Index() Action Method Recap

Metoda akcji Index() w klasie DinnersController wygląda obecnie następująco:

//
// GET: /Dinners/

public ActionResult Index() {

    var dinners = dinnerRepository.FindUpcomingDinners().ToList();
    return View(dinners);
}

Po wysłaniu żądania do adresu URL /Dinners pobiera listę wszystkich nadchodzących kolacji, a następnie renderuje listę wszystkich z nich:

Zrzut ekranu przedstawiający stronę listy Nerd Dinner Upcoming Dinner (Zbliżająca się kolacja).

Opis zapytania IQueryable<T>

Iqueryable<T> jest interfejsem wprowadzonym z LINQ w ramach platformy .NET 3.5. Umożliwia zaawansowane scenariusze "odroczonego wykonywania", z których możemy skorzystać w celu zaimplementowania obsługi stronicowania.

W naszym repozytorium DinnerRepository zwracamy sekwencję IQueryable<Dinner> z 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;
    }

Obiekt IQueryable<Dinner zwrócony przez metodę FindUpcomingDinners() hermetyzuje zapytanie w celu pobrania obiektów dinner> z bazy danych przy użyciu LINQ to SQL. Co ważne, zapytanie względem bazy danych nie zostanie wykonane, dopóki nie podejmiemy próby uzyskania dostępu do danych w zapytaniu lub do momentu wywołania metody ToList(). Kod wywołujący metodę FindUpcomingDinners() może opcjonalnie wybrać dodanie dodatkowych operacji/filtrów "łańcuchowych" do obiektu IQueryable<Dinner> przed wykonaniem zapytania. LINQ to SQL następnie jest wystarczająco inteligentny, aby wykonać połączone zapytanie względem bazy danych po zażądaniu danych.

Aby zaimplementować logikę stronicowania, możemy zaktualizować metodę akcji Index() dinnersController, aby zastosować dodatkowe operatory "Skip" i "Take" do zwróconej sekwencji kolacji> IQueryable<przed wywołaniem metody ToList():

//
// GET: /Dinners/

public ActionResult Index() {

    var upcomingDinners = dinnerRepository.FindUpcomingDinners();
    var paginatedDinners = upcomingDinners.Skip(10).Take(20).ToList();

    return View(paginatedDinners);
}

Powyższy kod pomija pierwsze 10 nadchodzących kolacji w bazie danych, a następnie zwraca 20 kolacji. LINQ to SQL jest wystarczająco inteligentny, aby utworzyć zoptymalizowane zapytanie SQL, które wykonuje tę logikę pomijania w bazie danych SQL — a nie na serwerze internetowym. Oznacza to, że nawet jeśli mamy miliony nadchodzących kolacji w bazie danych, tylko 10, które chcemy pobrać w ramach tego żądania (co czyni je wydajnym i skalowalnym).

Dodawanie wartości "page" do adresu URL

Zamiast trwale kodować określony zakres stron, chcemy, aby nasze adresy URL zawierały parametr "page", który wskazuje, którego zakresu kolacji żąda użytkownik.

Używanie wartości querystring

Poniższy kod pokazuje, jak możemy zaktualizować metodę akcji Index(), aby obsługiwać parametr querystring i włączyć adresy URL, takie jak /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);
}

Powyżej metoda akcji Index() ma parametr o nazwie "page". Parametr jest zadeklarowany jako liczba całkowita dopuszczana do wartości null (czyli co oznacza liczba całkowita? ). Oznacza to, że adres URL /Dinners?page=2 spowoduje przekazanie wartości "2" jako wartości parametru. Adres URL /Dinners (bez wartości ciągu zapytania) spowoduje przekazanie wartości null.

Pomnożymy wartość strony przez rozmiar strony (w tym przypadku 10 wierszy), aby określić liczbę kolacji do pominięcia. Używamy operatora "łączenia" w języku C# (??), który jest przydatny w przypadku typów dopuszczających wartość null. Powyższy kod przypisuje wartość strony 0, jeśli parametr strony ma wartość null.

Używanie wartości osadzonych adresów URL

Alternatywą dla użycia wartości ciągu zapytania jest osadzanie parametru strony w samym adresie URL. Na przykład: /Dinners/Page/2 lub /Dinners/2. ASP.NET MVC zawiera zaawansowany aparat routingu adresów URL, który ułatwia obsługę takich scenariuszy.

Możemy zarejestrować niestandardowe reguły routingu mapujące dowolny przychodzący adres URL lub format adresu URL na dowolną żądaną klasę kontrolera lub metodę akcji. Wystarczy otworzyć plik Global.asax w naszym projekcie:

Zrzut ekranu przedstawiający drzewo nawigacji Nerd Dinner. Globalna kropka a s x jest zaznaczona i wyróżniona.

Następnie zarejestruj nową regułę mapowania przy użyciu metody pomocniczej MapRoute(), takiej jak pierwsze wywołanie tras. Usługa MapRoute() poniżej:

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

Powyżej rejestrujemy nową regułę routingu o nazwie "UpcomingDinners". Wskazujemy, że ma on format adresu URL "Dinners/Page/{page}" — gdzie {page} jest wartością parametru osadzoną w adresie URL. Trzeci parametr metody MapRoute() wskazuje, że należy mapować adresy URL zgodne z tym formatem do metody akcji Index() w klasie DinnersController.

Możemy użyć dokładnie tego samego kodu Index(), który mieliśmy wcześniej w scenariuszu querystring — z wyjątkiem tego, że parametr "page" pochodzi z adresu URL, a nie ciągu zapytania:

//
// 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 teraz, gdy uruchomimy aplikację i wpiszemy /Dinners zobaczymy pierwsze 10 nadchodzących kolacji:

Zrzut ekranu przedstawiający listę Nerd Dinners Upcoming Dinners (Nadchodzące kolacje).

A kiedy wpiszemy ciąg /Dinners/Page/1 , zobaczymy następną stronę kolacji:

Zrzut ekranu przedstawiający następną stronę listy Nadchodzące kolacje.

Dodawanie interfejsu użytkownika nawigacji stron

Ostatnim krokiem do ukończenia naszego scenariusza stronicowania będzie zaimplementowanie "dalej" i "poprzedniego" interfejsu użytkownika nawigacji w naszym szablonie widoku, aby umożliwić użytkownikom łatwe pomijanie danych kolacji.

Aby zaimplementować to poprawnie, musimy znać łączną liczbę kolacji w bazie danych, a także liczbę stron danych, na które się przekłada. Następnie musimy obliczyć, czy obecnie żądana wartość "page" znajduje się na początku lub na końcu danych, a następnie pokaż lub ukryj "poprzedni" i "następny" interfejs użytkownika odpowiednio. Możemy zaimplementować tę logikę w naszej metodzie akcji Index(). Alternatywnie możemy dodać klasę pomocnika do naszego projektu, która hermetyzuje tę logikę w sposób bardziej wielokrotnego użytku.

Poniżej znajduje się prosta klasa pomocnika "PaginatedList", która pochodzi z klasy kolekcji List<T> wbudowanej w .NET Framework. Implementuje klasę kolekcji wielokrotnego użytku, która może służyć do stronicowania dowolnej sekwencji danych IQueryable. W naszej aplikacji NerdDinner będziemy mieli, że będzie ona działać nad wynikami IQueryable<Dinner>, ale może być tak samo łatwo używana w przypadku produktu> IQueryable<lub IQueryable<Customer> wyniki w innych scenariuszach aplikacji:

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

Zwróć uwagę na sposób obliczania, a następnie uwidacznia właściwości, takie jak "PageIndex", "PageSize", "TotalCount" i "TotalPages". Następnie uwidacznia dwie właściwości pomocnicze "HasPreviousPage" i "HasNextPage", które wskazują, czy strona danych w kolekcji znajduje się na początku, czy na końcu oryginalnej sekwencji. Powyższy kod spowoduje uruchomienie dwóch zapytań SQL — pierwszy, aby pobrać liczbę całkowitej liczby obiektów Dinner (nie zwraca obiektów — zamiast tego wykonuje instrukcję "SELECT COUNT", która zwraca liczbę całkowitą), a drugą, aby pobrać tylko wiersze danych potrzebnych z naszej bazy danych dla bieżącej strony danych.

Następnie możemy zaktualizować metodę pomocnika DinnersController.Index(), aby utworzyć kolację> PaginatedList<z naszego wyniku DinnerRepository.FindUpcomingDinners() i przekazać ją do naszego szablonu widoku:

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

Następnie możemy zaktualizować szablon widoku \Views\Dinners\Index.aspx, aby dziedziczył z widoku ViewPage<NerdDinner.Helpers.PaginatedList<Dinner>> zamiast viewPage<IEnumerable<Dinner>>, a następnie dodać następujący kod do dołu naszego szablonu widoku, aby pokazać lub ukryć następny i poprzedni interfejs użytkownika nawigacji:

<% if (Model.HasPreviousPage) { %>

    <%= Html.RouteLink("<<<", "UpcomingDinners", new { page = (Model.PageIndex-1) }) %>

<% } %>

<% if (Model.HasNextPage) {  %>

    <%= Html.RouteLink(">>>", "UpcomingDinners", new { page = (Model.PageIndex + 1) }) %>

<% } %>

Zwróć uwagę na to, jak używamy metody pomocnika Html.RouteLink() do generowania naszych hiperlinków. Ta metoda jest podobna do metody pomocniczej Html.ActionLink(), która została wcześniej użyta. Różnica polega na tym, że generujemy adres URL przy użyciu reguły routingu "UpcomingDinners", którą konfigurujemy w naszym pliku Global.asax. Dzięki temu wygenerujemy adresy URL w metodzie akcji Index(), która ma format: /Dinners/Page/{page} — gdzie wartość {page} jest zmienną dostarczaną powyżej na podstawie bieżącego indeksu strony.

A teraz, gdy ponownie uruchomimy aplikację, w przeglądarce zobaczymy 10 kolacji:

Zrzut ekranu przedstawiający listę Nadchodzących kolacji na stronie Kolacja Nerd.

Mamy również <<< interfejs użytkownika nawigacji w >>> dolnej części strony, który umożliwia pomijanie danych do przodu i do tyłu przy użyciu adresów URL dostępnych dla wyszukiwarki:

Zrzut ekranu przedstawiający stronę Kolacje Nerd z listą Nadchodzące kolacje.

Temat boczny: Zrozumienie skutków zapytania IQueryable<T>
Funkcja IQueryable<T> to bardzo zaawansowana funkcja umożliwiająca różne interesujące odroczone scenariusze wykonywania (takie jak zapytania oparte na stronicowaniu i kompozycji). Podobnie jak w przypadku wszystkich zaawansowanych funkcji, chcesz zachować ostrożność podczas korzystania z niej i upewnić się, że nie jest nadużywany. Należy pamiętać, że zwracanie wyniku IQueryable<T> z repozytorium umożliwia wywoływanie kodu w celu dołączenia do niego metod operatorów łańcuchowych, a więc uczestnictwo w ostatecznym wykonaniu zapytania. Jeśli nie chcesz podawać kodu wywołującego tę możliwość, należy zwrócić wyniki IList<T> lub IEnumerable<T> , które zawierają wyniki zapytania, które zostało już wykonane. W przypadku scenariuszy stronicowania wymaga to wypchnięcia rzeczywistej logiki stronicowania danych do wywoływanej metody repozytorium. W tym scenariuszu możemy zaktualizować metodę wyszukiwania FindUpcomingDinners(), aby uzyskać podpis, który zwrócił element PaginatedList: PaginatedList< Dinner> FindUpcomingDinners(int pageIndex, int pageSize) { } Lub wróć do kolacji> IList<i użyj parametru "totalCount", aby zwrócić łączną liczbę kolacji: IList<Dinner> FindUpcomingDinners(int pageIndex, int pageSize, out int totalCount) { }

Następny krok

Teraz przyjrzyjmy się, jak możemy dodać obsługę uwierzytelniania i autoryzacji do naszej aplikacji.