Поделиться через


Реализация эффективного разбиения данных по страницам

от Корпорации Майкрософт

Загрузить PDF-файл

Это шаг 8 бесплатного руководства по приложению "NerdDinner" , в которых показано, как создать небольшое, но полное веб-приложение с помощью ASP.NET MVC 1.

Шаг 8 показывает, как добавить поддержку разбиения по страницам в наш URL-адрес /Dinners, чтобы вместо отображения 1000-х ужинов одновременно, мы будем отображать только 10 предстоящих ужинов за раз - и позволить конечным пользователям страницы назад и вперед через весь список в SEO дружественным способом.

Если вы используете ASP.NET MVC 3, рекомендуем следовать руководствам по начало работы С MVC 3 или MVC Music Store.

NerdDinner, шаг 8. Поддержка разбиения по страницам

Если наш сайт будет успешным, он будет иметь тысячи предстоящих ужинов. Мы должны убедиться, что наш пользовательский интерфейс масштабируется для обработки всех этих ужинов и позволяет пользователям просматривать их. Чтобы включить это, мы добавим поддержку разбиения по страницам в наш URL-адрес /Dinners , чтобы вместо отображения 1000s ужинов одновременно, мы будем отображать только 10 предстоящих ужинов за раз - и позволить конечным пользователям страницы назад и вперед через весь список в SEO дружественным способом.

Index() Action Method Recap

Метод действия Index() в классе DinnersController в настоящее время выглядит следующим образом:

//
// GET: /Dinners/

public ActionResult Index() {

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

При выполнении запроса по URL-адресу /Dinners он получает список всех предстоящих ужинов, а затем отображает список всех из них:

Снимок экрана: страница списка предстоящих ужинов Nerd Dinner.

Основные сведения об IQueryable<T>

Iqueryable<T> — это интерфейс, который был представлен с LINQ в составе .NET 3.5. Это обеспечивает эффективные сценарии "отложенного выполнения", которые можно использовать для реализации поддержки разбиения по страницам.

В нашем DinnerRepository мы возвращаем последовательность IQueryable<Dinner> из нашего метода 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;
    }

Объект IQueryable<Dinner>, возвращаемый методом FindUpcomingDinners(), инкапсулирует запрос для получения объектов Dinner из базы данных с помощью LINQ to SQL. Важно отметить, что он не будет выполнять запрос к базе данных, пока мы не попытаемся получить доступ к данным в запросе или выполнить итерацию по ним, или пока мы не вызовем метод ToList(). Код, вызывающий наш метод FindUpcomingDinners(), может при необходимости добавить дополнительные "связанные" операции или фильтры в объект IQueryable<Dinner> перед выполнением запроса. LINQ to SQL достаточно умен, чтобы выполнить объединенный запрос к базе данных при запросе данных.

Чтобы реализовать логику разбиения по страницам, мы можем обновить метод действия Index() в DinnersController, чтобы он применял дополнительные операторы Skip и Take к возвращаемой последовательности IQueryable Dinner> перед вызовом<ToList() в ней:

//
// GET: /Dinners/

public ActionResult Index() {

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

    return View(paginatedDinners);
}

Приведенный выше код пропускает первые 10 предстоящих ужинов в базе данных, а затем возвращает 20 ужинов. LINQ to SQL достаточно умен, чтобы создать оптимизированный SQL-запрос, который выполняет эту логику пропуска в базе данных SQL, а не на веб-сервере. Это означает, что даже если у нас есть миллионы предстоящих ужинов в базе данных, только 10 из них будут получены в рамках этого запроса (что делает его эффективным и масштабируемым).

Добавление значения "page" в URL-адрес

Вместо жесткого программирования определенного диапазона страниц мы хотим, чтобы наши URL-адреса включали параметр page, указывающий, какой диапазон Dinner запрашивается пользователем.

Использование значения querystring

В приведенном ниже коде показано, как обновить метод действия Index() для поддержки параметра querystring и включения URL-адресов , таких как /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);
}

Приведенный выше метод действия Index() имеет параметр с именем page. Параметр объявляется как целое число, допускающее значение NULL (что указывает int? ). Это означает, что URL-адрес /Dinners?page=2 приведет к передаче значения "2" в качестве значения параметра. URL-адрес /Dinners (без значения querystring) приведет к передаче значения NULL.

Мы умножаем значение страницы на размер страницы (в данном случае 10 строк), чтобы определить, сколько ужинов следует пропустить. Мы используем оператор объединения c# null (??), который полезен при работе с типами, допускающими значение NULL. Приведенный выше код присваивает странице значение 0, если параметр страницы имеет значение NULL.

Использование значений внедренного URL-адреса

Вместо использования значения строки запроса можно было бы внедрить параметр страницы в сам URL-адрес. Например: /Dinners/Page/2 или /Dinners/2. ASP.NET MVC включает мощный механизм маршрутизации URL-адресов, который упрощает поддержку таких сценариев.

Мы можем зарегистрировать настраиваемые правила маршрутизации, которые сопоставляют любой входящий URL-адрес или формат URL-адреса с любым классом контроллера или методом действия, который нам нужен. Все, что нам нужно сделать, это открыть файл Global.asax в нашем проекте:

Снимок экрана: дерево навигации Nerd Dinner. Выделена и выделена глобальная точка s a x.

А затем зарегистрируйте новое правило сопоставления с помощью вспомогательного метода MapRoute(), например при первом вызове маршрутов. MapRoute() ниже:

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

Выше мы регистрируем новое правило маршрутизации с именем UpcomingDinners. Мы указываем, что он имеет формат URL-адреса "Dinners/Page/{page}", где {page} — это значение параметра, внедренное в URL-адрес. Третий параметр метода MapRoute() указывает, что следует сопоставить URL-адреса, соответствующие этому формату, с методом действия Index() в классе DinnersController.

Мы можем использовать тот же код Index(), который мы использовали ранее в нашем сценарии querystring, за исключением того, что теперь наш параметр page будет исходить из URL-адреса, а не строки запроса:

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

А теперь, когда мы запустите приложение и введем /Dinners , мы увидим первые 10 предстоящих ужинов:

Снимок экрана: список предстоящих ужинов Nerd Dinners.

И когда мы введем /Dinners/Page/1 , мы увидим следующую страницу ужинов:

Снимок экрана: следующая страница списка предстоящих ужинов.

Добавление пользовательского интерфейса навигации по страницам

Последним шагом для выполнения сценария разбиения на страницы будет реализация пользовательского интерфейса навигации "next" и "previous" в нашем шаблоне представления, чтобы пользователи могли легко пропустить данные Dinner.

Чтобы правильно реализовать это, нам нужно знать общее количество обедов в базе данных, а также сколько страниц данных это преобразует. Затем нам потребуется вычислить, находится ли запрошенное в настоящее время значение "page" в начале или конце данных, и отобразить или скрыть пользовательский интерфейс "previous" и "next" соответственно. Эту логику можно реализовать в методе действия Index(). Кроме того, мы можем добавить в проект вспомогательный класс, который инкапсулирует эту логику более пригодным для повторного использования способом.

Ниже приведен простой вспомогательный класс PaginatedList, производный от класса коллекции List<T>, встроенного в платформа .NET Framework. Он реализует повторно используемый класс коллекции, который можно использовать для разности любой последовательности данных IQueryable. В нашем приложении NerdDinner он будет работать над результатами IQueryable<Dinner> , но его можно так же легко использовать для результатов IQueryable<Product> или IQueryable<Customer> в других сценариях приложений:

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

Обратите внимание, как он вычисляет, а затем предоставляет такие свойства, как "PageIndex", "PageSize", "TotalCount" и "TotalPages". Затем он также предоставляет два вспомогательных свойства HasPreviousPage и HasNextPage, которые указывают, находится ли страница данных в коллекции в начале или конце исходной последовательности. Приведенный выше код приведет к выполнению двух SQL-запросов: первый для получения общего числа объектов Dinner (это не возвращает объекты , а выполняет инструкцию SELECT COUNT, которая возвращает целое число), а второй — для получения только строк данных, необходимых из нашей базы данных для текущей страницы данных.

Затем мы можем обновить наш вспомогательный метод DinnersController.Index(), чтобы создать ужин PaginatedList<> из результата DinnerRepository.FindUpcomingDinners() и передать его в шаблон представления:

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

Затем можно обновить шаблон представления \Views\Dinners\Index.aspx для наследования от ViewPage<NerdDinner.Helpers.PaginatedList<Dinner>> вместо ViewPage<IEnumerable<Dinner>>, а затем добавить следующий код в нижнюю часть нашего шаблона представления, чтобы отобразить или скрыть следующий и предыдущий пользовательский интерфейс навигации:

<% if (Model.HasPreviousPage) { %>

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

<% } %>

<% if (Model.HasNextPage) {  %>

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

<% } %>

Обратите внимание, что мы используем вспомогательный метод Html.RouteLink() для создания гиперссылок. Этот метод аналогичен вспомогательному методу Html.ActionLink(), который мы использовали ранее. Разница заключается в том, что мы создаем URL-адрес с помощью правила маршрутизации UpcomingDinners, которое мы настраиваем в файле Global.asax. Это гарантирует, что мы создадим URL-адреса для метода действия Index() в формате /Dinners/Page/{page} , где значение {page} является переменной, которую мы предоставляем выше на основе текущего объекта PageIndex.

И теперь, когда мы снова запустите наше приложение, мы увидим 10 ужинов за раз в нашем браузере:

Снимок экрана: список предстоящих ужинов на странице

У нас также есть <<<>>> пользовательский интерфейс навигации в нижней части страницы, который позволяет нам переходить вперед и назад по нашим данным с помощью URL-адресов, доступных для поисковой системы:

Снимок экрана: страница

Побочного раздела: общие сведения о последствиях IQueryable<T>
IQueryable<T> — это очень мощная функция, которая обеспечивает множество интересных сценариев отложенного выполнения (например, разбиение по страницам и запросы на основе композиции). Как и в случае со всеми мощными функциями, вы хотите быть осторожными с их использованием и убедиться, что он не злоупотребляется. Важно понимать, что возврат результата IQueryable<T> из репозитория позволяет вызывающему коду добавлять в него методы цепочки операторов и таким образом участвовать в конечном выполнении запроса. Если вы не хотите предоставлять возможность вызова кода, следует вернуть результаты IList<T> или IEnumerable<T> , содержащие результаты уже выполненного запроса. Для сценариев разбиения на страницы потребуется отправить фактическую логику разбиения данных на страницы в вызываемый метод репозитория. В этом сценарии мы можем обновить наш метод finder FindUpcomingDinners() для получения сигнатуры, которая возвращает paginatedList: PaginatedList< Dinner> FindUpcomingDinners(int pageIndex, int pageSize) { } Или вернуть ужин IList>< и использовать параметр totalCount out, чтобы вернуть общее количество ужинов: IList<Dinner> FindUpcomingDinners(int pageIndex, int pageSize, out int totalCount) { }

Следующий шаг

Теперь давайте рассмотрим, как добавить поддержку проверки подлинности и авторизации в наше приложение.