Разбиение на страницы GridView

162

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

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

Элемент управления GridView обладает встроенной поддержкой разбиения на страницы. Простое разбиение на страницы можно применять как в SqlDataSource, так и в ObjectDataSource. Если вы используете ObjectDataSource, то также имеете возможность настроить работу этого механизма для получения наиболее эффективных и масштабируемых решений.

Автоматическое разбиение на страницы

Установив несколько свойств и обрабатывая событие, можно заставить элемент GridView управлять разбиением на страницы. Элемент GridView создаст ссылки для перемещения на предыдущую и последующую страницы, и будет отображать записи текущей страницы, не требуя извлекать их вручную. Прежде чем мы обсудим преимущества и недостатки такого подхода, давайте посмотрим, что необходимо сделать для работы этого механизма.

GridView предоставляет несколько свойств, спроектированных специально для поддержки разбиения на страницы. Эти свойства перечислены в таблице ниже:

Члены GridView, имеющие отношение к разбиению на страницы
Свойство Описание
AllowPaging

Включает или отключает разбиение на страницы привязанных записей. По умолчанию равно false

PageSize

Получает или устанавливает количество элементов для отображения на одной странице сетки. По умолчанию равно 10

CurrentPageIndex

Получает или устанавливает начинающийся с нуля индекс текущей отображаемой страницы, если разбиение на страницы включено

PagerSettings

Поставляет объект PagerSettings, который служит оболочкой множества опций форматирования. Эти опции определяют местоположение элементов управления страницами, их текст или изображения. Эти свойства можно устанавливать для тонкой настройки внешнего вида этих элементов управления страницами либо использовать значения по умолчанию

PagerStyle

Предоставляет объект стиля, который можно использовать для конфигурирования шрифтов, цветов и выравнивания текста для элементов управления страницами

События PageIndexChanging и PageIndexChanged

Происходят, когда выполняется щелчок на одном из элементов выбора страниц — перед навигацией (PageIndexChanging) и после нее (PageIndexChanged)

Чтобы использовать автоматическое разбиение на страницы, следует установить свойство AllowPaging в true (что покажет элементы управления страницами), а также установить PageSize для определения количества строк, отображаемых на каждой странице. Если не установить свойство PageSize явно, по умолчанию используется значение 10.

Ниже представлен пример объявления элемента управления GridView, в котором устанавливаются эти свойства и источник данных:

<asp:GridView ID="gridEmployees" runat="server" DataSourceID="getProductsSDS" DataKeyNames="ProductID"
            AllowPaging="true" PageSize="5" ...>

            <Columns>
                <asp:BoundField HeaderText="ID" DataField="ProductID" />
                <asp:BoundField HeaderText="Название" DataField="ProductName" />
                <asp:BoundField HeaderText="Цена" DataField="UnitPrice" DataFormatString="{0:c}" />
                <asp:BoundField HeaderText="Кол-во на складе" DataField="UnitsInStock"
                    ItemStyle-HorizontalAlign="Right" />
            </Columns>
            ...
</asp:GridView>
        
<asp:SqlDataSource ID="getProductsSDS" runat="server"
	ConnectionString="<%$ ConnectionStrings:Northwind %>"
	SelectCommand="SELECT ProductID, ProductName, UnitPrice, UnitsInStock FROM Products" />

Этого достаточно, чтобы начать использовать разбиение на страницы. На рисунке ниже показан пример с пятью записями на странице (всего 16 страниц):

Разбиение на страницы по пять записей

Автоматическое разбиение на страницы работает с любыми источниками данных, которые реализуют интерфейс ICollection. Это значит, что SqlDataSource поддерживает этот механизм до тех пор, пока используется режим DataSet (режим DataReader работать не будет и вызовет исключение). Вдобавок ObjectDataSource также поддерживает разбиение на страницы, предполагая, что метод пользовательского класса доступа к данным возвращает объект, реализующий ICollection; массивы, строго типизированные коллекции, а также автономные DataSet — все это допустимые варианты.

Автоматическое разбиение на страницы не сокращает объем данных, которые необходимо запросить из базы данных. Каждый раз, когда пользователь осуществляет навигацию к другой странице, извлекаются и привязываются все данные. Другими словами, если таблица разделена на 10 страниц, и пользователь перелистает каждую из них, то в результате получится, что 10 раз будет выполнена одна и та же работа (и вся нагрузка на базу данных, связанная с выполнением этого запроса, также увеличится в 10 раз).

К счастью, автоматическое разбиение на страницы можно сделать более эффективно, включив автоматическое кэширование для элемента управления источником данных (будет рассказано позже). Это позволит повторно использовать один и тот же объект данных для многих запросов. Конечно, сохранение данных в кэше может оказаться не идеальным решением, если разбиение на страницы применяется с чрезвычайно большими запросами. В этом случае объем памяти, необходимый для сохранения полного DataSet в кэше, становится недопустимо большим. И здесь на сцену выходит специальное пользовательское разбиение на страницы.

Специальное разбиение на страницы с помощью ObjectDataSource

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

Хотя специальное разбиение на страницы сложнее, чем автоматическое, оно также помогает минимизировать нагрузку на сеть и избежать сохранения огромных объектов данных в памяти на стороне сервера. С другой стороны, большинство стратегий специального разбиения на страницы требует обращения к базе данных при каждой обратной отправке, а это означает увеличение нагрузки на базу данных.

ObjectDataSource — единственный источник данных, который поддерживает специальное разбиение страниц. Первым шагом к управлению разбиением на страницы является установка свойства ObjectDataSource.EnablePaging в true. Затем разбиение на страницы реализуется тремя дополнительными свойствами: StartRowIndexParameterName, MaximumRowsParameterName и SelectCountMethod.

Чтобы определить, когда специальное разбиение на страницы лучше, чем автоматическое с кэшированием, необходимо оценить способы использований данных. Чем больше объем данных, отображаемых GridView, тем более вероятно, что лучше применить специальное разбиение. С другой стороны, чем медленнее сервер базы данных и выше его нагрузка, тем более вероятно, что вы захотите уменьшить количество повторных вызовов посредством кэширования полного объекта данных. В конечном итоге, возможно, потребуется выполнить профилирование приложения и определить оптимальную стратегию разбиения страниц.

Подсчет записей

Чтобы GridView мог создать правильное количество ссылок на страницы, он должен знать общее количество записей и число записей на странице. Количество записей на страницу устанавливается в свойстве PageSize, как было показано в предыдущем примере. С общим количеством записей ситуация несколько сложнее.

При использовании автоматического разбиения на страницы общее число записей автоматически определяется GridView на основе количества записей в источнике данных. При специальном разбиении на страницы необходимо явно подсчитывать общее число с использованием выделенного метода. Класс EmployeeDB (спроектированный в статье «Компонент доступа к данным») уже включает метод CountEmployees(), возвращающий эту информацию. Необходимо просто привязать этот метод к ObjectDataSurce с помощью свойства SelectCountMethod:

<asp:ObjectDataSource ID="getEmployeesODS" runat="server"
	TypeName="EmployeeDB" SelectMethod="GetEmployees"
	EnablePaging="true" SelectCountMethod="CountEmployees"/>

Когда применяется специальное разбиение на страницы, метод SelectCountMethod выполняется при каждой обратной отправке. Если вы хотите снизить нагрузку на базу, но с риском получения неверного количества записей, можете кэшировать эту информацию и повторно использовать ее.

Хранимая процедура для получения страницы записей

Следующая часть решения немного хитрее. Вместо извлечения коллекции записей о сотрудниках метод GetEmployees() должен извлекать записи только для текущей страницы. Чтобы обеспечить это, в настоящем примере используется хранимая процедура GetEmployeePage. Она копирует все записи о сотрудниках во временную таблицу, имеющую один дополнительный столбец — уникальный автоинкрементный идентификатор, номерующий каждую строку. Затем хранимая процедура производит выборку из этой таблицы, соответствующую запрошенной странице данных, с применением параметров @Start и @Count.

Ниже приведен полный код хранимой процедуры:

CREATE PROCEDURE GetEmployeePage 
	@Start int, @Count int 
AS

-- Создать временную таблицу с интересующими столбцами
CREATE TABLE #TempEmployees (
	ID int IDENTITY PRIMARY KEY, 
	EmployeeID int, 
	LastName nvarchar(20), 
	FirstName nvarchar(10),
	BirthDate datetime
)

-- Заполнить временную таблицу записями обо всех сотрудниках
INSERT INTO #TempEmployees
(
	EmployeeID, LastName, FirstName, BirthDate
)
SELECT
	EmployeeID, LastName, FirstName, BirthDate
FROM
	Employees ORDER BY EmployeeID ASC 

-- Объявить две переменных для вычисления диапазона записей,
-- которые нужно извлечь для указанной страницы
DECLARE @FromID int
DECLARE @ToID int

-- Вычислить первый и последний идентификаторы необходимого диапазона записей
SET @FromID = @Start
SET @ToID = @Start + @Count - 1

-- Извлечь страницу записей
SELECT * FROM #TempEmployees WHERE ID >= @FromID AND ID <= @ToID

В этой хранимой процедуре применяется подход, специфичный для SQL Server. Другие базы данных могут предлагать иные способы оптимизации. Например, Oracle позволяет использовать оператор ROWNUM в конструкции WHERE запроса для возврата диапазона строк. Например, запрос SELECT * FROM Employee WHERE ROWNUM > 100 AND ROWNUM < 200 извлечет страницу строк со 101-й по 199-ю.

Метод постраничной выборки

Последний шаг предусматривает создание перегрузки метода GetEmployees(), выполняющего разбиение на страницы. Этот метод принимает два аргумента: индекс строки, с которой начинается страница (начинающийся с 0), и размер страницы (максимальное количество строк). Имена параметров, которые должны использоваться для этих двух деталей, указываются через свойства StartRowIndexParameterName и MaximumRowsParameterName элемента управления ObjectDataSource. Если они не установлены, по умолчанию применяются startRowIndex и maximumRows.

Ниже представлен код метода GetEmployees(), который необходим для использования хранимых процедур, показанных в предыдущем примере:

public List<EmployeeDetails> GetEmployees(int startRowIndex, int maximumRows)
{
        SqlConnection con = new SqlConnection(connectionString);
        SqlCommand cmd = new SqlCommand("GetEmployeePage", con);
        cmd.CommandType = CommandType.StoredProcedure;
        cmd.Parameters.Add(new SqlParameter("@Start", SqlDbType.Int, 4));
        cmd.Parameters["@Start"].Value = startRowIndex + 1;
        cmd.Parameters.Add(new SqlParameter("@Count", SqlDbType.Int, 4));
        cmd.Parameters["@Count"].Value = maximumRows;

        // Создать коллекцию для всех записей о сотрудниках
        List<EmployeeDetails> employees = new List<EmployeeDetails>();

        try
        {
            con.Open();
            SqlDataReader reader = cmd.ExecuteReader();

            while (reader.Read())
            {
                EmployeeDetails emp = new EmployeeDetails(
                    (int)reader["EmployeeID"], (string)reader["FirstName"],
                    (string)reader["LastName"], (DateTime)reader["BirthDate"]);
                employees.Add(emp);
            }
            reader.Close();

            return employees;
        }
        catch (SqlException)
        {
            throw new ApplicationException("Ошибка данных");
        }
        finally
        {
            con.Close();
        }
}

Запустив эту страницу, вы увидите, что выход будет точно таким же, как и тот, что генерирует предыдущая страница, которая использует автоматическое разбиение, и элементы управления страницами работают совершенно аналогично.

Настройка панели страниц

Элементы управления страницами GridView исключительно гибки. По умолчанию они представляют собой серию пронумерованных ссылок. Однако их можно настроить по своему вкусу, используя свойства PagerStyle (речь идет о цветах фона и текста, шрифте, размере и т.п.) и PagerSettings:

<asp:GridView ID="gridEmployees" runat="server" ...>
	<PagerSettings Mode="NextPrevious" PreviousPageText="<< Назад" 
		NextPageText="Вперед >>" />
	<PagerStyle BackColor="#FFCC66" ForeColor="#444" HorizontalAlign="Center" />
    ...
</asp:GridView>
Стилизация постраничной навигации

Наиболее важная деталь здесь является свойство PagerSettings.Mode, определяющее, как отображать страничные ссылки в соответствии с одним из нескольких стилей, описанных в таблице ниже:

Стили панели управления страницами (PagerSettings.Mode)
Режим Описание
Numeric

Панель отображается в виде множества ссылок на другие страницы, как указано в свойстве PageButtonCount. Если этого количества ссылок недостаточно для перечисления всех страниц, в панели присутствуют ссылки в виде троеточия (...), щелчок на которых приводит к отображению предыдущего или следующего набора ссылок страниц

NextPrevious

Отображаются только две ссылки для перехода на предыдущую и следующую страницы. Если выбрать эту опцию, то можно также определить текст этих двух ссылок через свойства NextPageText и PreviousPageText (или использовать ссылки в виде изображений через NextPageImageUrl и PreviousPageImageUrl)

NumericFirstLast

To же самое, что и Numeric, за исключением того, что присутствуют дополнительные ссылки на первую и последнюю страницы

NextPreviousFirstLast

То же самое, что и NextPrevious, за исключением того, что присутствуют дополнительные ссылки на первую и последнюю страницы. Эти ссылки можно установить с помощью свойств NextPageText и PreviousPageText (или использовать изображения через NextPageImageUrl и PreviousPageImageUrl)

Если вам не нравится панель управления страницами по умолчанию, можете реализовать собственную, используя средство шаблонов, которое описано в следующей статье, посредством создания PagerTemplate. После этого можно использовать любой элемент управления по своему желанию, например, текстовое поле, в котором пользователь будет вводить индекс страницы, и кнопку для отправки запроса и загрузки новой страницы. Код для извлечения и привязки записей текущей страницы останется прежним.

Обратные вызовы для сортировки и разбиения на страницы

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

GridView имеет средство, которое позволяет справиться с этой проблемой: свойство EnableSortingAndPagingCallbacks. Если его установить в true, то GridView будет применять другую технологию для обновления страницы. Вместо того чтобы принудительно выполнять обратную отправку при щелчке на заголовке столбца или на страничной ссылке, браузер посылает асинхронный запрос серверу для получения новой информации. Когда браузер получает эту новую информацию, он модифицирует текущую страницу, используя HTML DOM. Этот прием обеспечивает более гладкое, лишенное мерцаний поведение.

Лучше всего то, что если браузер не поддерживает это средство, то GridView благополучно возвращается к стандартной модели поведения с обратными отправками. Единственное ограничение в том, что вы не можете применять обратные вызовы сортировки и разбиения на страницы в сетке, использующей шаблоны.

Свойство EnableSortingAndPagingCallbacks пользуется инфраструктурой обратных вызовов ASP.NET. "За кулисами" обратные вызовы осуществляются с использованием JavaScript и объекта XMLHttpRequest (AJAX).

Пройди тесты
Лучший чат для C# программистов