В этой статье я ознакомлю вас с WebGrid и покажу, как его можно использовать в ASP.NET MVC 3; затем мы рассмотрим, как выжать из него максимум возможного в решении на основе ASP.NET MVC. (Обзор WebMatrix и синтаксис Razor, который я буду использовать здесь, см. в статье Кларка Селла «Введение в WebMatrix» в апрельском номере по ссылке msdn.microsoft.com/ru-ru/magazine/gg983489.aspx.)
Вы также увидите, как компонент WebGrid интегрируется в среду ASP.NET MVC и упрощает разработчикам рендеринг табличных данных. Основное внимание я буду уделять рассмотрению WebGrid в разрезе ASP.NET MVC: созданию строго типизированной версии WebGrid с полной поддержкой IntelliSense, использованию точек подключения к WebGrid для поддержки разбиения на страницы на серверной стороне и добавлению функциональности AJAX. Рабочие примеры опираются на сервис, который предоставляет доступ к базе данных AdventureWorksLT через Entity Framework. Если вас интересует код доступа к данным, вы можете просмотреть его в пакете исходного кода, который можно скачать для этой статьи. Кроме того, я советую прочитать статью Джули Лерман «Разбиение на страницы на серверной стороне с применением Entity Framework и ASP.NET MVC 3» в мартовском номере (msdn.microsoft.com/ru-ru/magazine/gg650669).
Очень часто при рендеринге списка элементов нужно давать возможность пользователям щелкать любой из элементов для перехода в представление Details.
Приступаем к работе с WebGrid
Чтобы показать простой пример с WebGrid, я подготовил простую операцию ASP.NET MVC, которая передает IEnumerable<Product> в представление. В большей части статьи я использую механизм представлений Razor, но потом мы обсудим еще и механизм представлений Web Forms. Мой класс ProductController имеет следующую операцию:
public ActionResult List()
{
IEnumerable<Product> model = _productService.GetProducts();
return View(model);
}
Представление List включает следующий Razor-код, который выполняет рендеринг сетки, показанной на рис. 1:
@model IEnumerable<MsdnMvcWebGrid.Domain.Product>
@{
ViewBag.Title = "Basic Web Grid";
}
<h2>Basic Web Grid</h2>
<div>
@{
var grid = new WebGrid(Model, defaultSort:"Name");
}
@grid.GetHtml()
</div>
Рис. 1. Рендеринг Basic Web Grid
В первой строке представления указывается тип модели (например, тип свойства Model, к которому мы обращаемся в представлении) — IEnumerable<Product>. Затем внутри элемента div я создаю экземпляр WebGrid, передавая данные модели; я делаю это в блоке кода @{...}, чтобы Razor не пытался осуществлять рендеринг результата. В конструкторе я также присваиваю параметру defaultSort значение «Name», чтобы WebGrid знал, что передаваемые данные уже отсортированы по Name. Наконец, я использую @grid.GetHtml(), чтобы сгенерировать HTML для сетки и выполнить его рендеринг в ответе.
Этот небольшой фрагмент кода создает весьма богатую функциональность сетки. Сетка ограничивает объем отображаемых данных и включает ссылки элемента управления страницами (pager) для перемещения по данным; заголовки столбцов визуализируются как ссылки для поддержки «пролистывания» страниц. Для настройки этого поведения можно указать ряд параметров в конструкторе WebGrid и методе GetHtml. Эти параметры позволяют отключать разбиение на страницы и сортировку, изменять количество строк на страницу, заменять текст в ссылках элемента управления страницами и многое другое. В табл. 1 показаны параметры конструктораWebGrid, а в табл. 2 — параметры методаGetHtml.
Табл. 1. Параметры конструктора WebGrid
Имя | Тип | Описание |
source | IEnumerable<dynamic> | Данные, подлежащие рендерингу |
columnNames | IEnumerable<string> | Фильтрует выводимые столбцы |
defaultSort | string | Указывает столбец по умолчанию, по которому осуществляется сортировка |
rowsPerPage | int | Управляет тем, сколько строк (записей) отображается на каждой странице (по умолчанию — 10) |
canPage | bool | Включает или отключает разбиение данных на страницы |
canSort | bool | Включает или отключает сортировку данных |
ajaxUpdateContainerId | string | Идентификатор элемента-контейнера для сетки, обеспечивающего поддержку AJAX |
ajaxUpdateCallback | string | Клиентская функция, вызываемая при завершении AJAX-обновления |
fieldNamePrefix | string | Префикс для полей строк запроса (для поддержки нескольких сеток) |
pageFieldName | string | Имя поля строки запроса для номера страницы |
selectionFieldName | string | Имя поля строки запроса для выбранного номера записи |
sortFieldName | string | Имя поля строки запроса для столбца, по которому осуществляется сортировка |
sortDirectionFieldName | string | Имя поля строки запроса для направления сортировки |
Табл. 2. Параметры метода WebGrid.GetHtml
Имя | Тип | Описание |
tableStyle | string | Класс таблицы для применения стилей |
headerStyle | string | Класс строки заголовка для применения стилей |
footerStyle | string | Класс строки нижнего колонтитула для применения стилей |
rowStyle | string | Класс строки для применения стилей (только нечетные строки) |
alternatingRowStyle | string | Класс строки записи для применения стилей (только четные строки) |
selectedRowStyle | string | Выбранный класс строки для применения стилей |
caption | string | Строка, отображаемая в названии таблицы |
displayHeader | bool | Указывает, надо ли отображать строку заголовка |
fillEmptyRows | bool | Указывает, можно ли добавлять в таблицу пустые строки, чтобы соблюсти количество строк на страницу (rowsPerPage) |
emptyRowCellValue | string | Значение, используемое для заполнения пустых строк; используется, только когда установлен флаг fillEmptyRows |
columns | IEnumerable<WebGridColumn> | Модель столбца для настройки рендеринга столбцов |
exclusions | IEnumerable<string> | Столбцы, исключаемые из процесса автоматического заполнения столбцов |
mode | WebGridPagerModes | Режимы рендеринга элемента управления страницами (по умолчанию — NextPrevious и Numeric) |
firstText | string | Текст ссылки на первую страницу |
previousText | string | Текст ссылки на предыдущую страницу |
nextText | string | Текст ссылки на следующую страницу |
lastText | string | Текст ссылки на последнюю страницу |
numericLinksCount | int | Количество отображаемых ссылок-номеров (по умолчанию — 5) |
htmlAttributes | object | Содержит HTML-атрибуты, задаваемые для элемента |
Параметр format метода Column позволяет настраивать рендеринг элемента данных.
Предыдущий Razor-код будет осуществлять рендеринг всех свойств для каждой строки (записи), но вы, вероятно, захотите ограничить количество отображаемых полей. Этого можно добиться несколькими способами. Первый (и самый простой) — передать набор полей конструктору WebGrid. Например, показанный ниже код выполняет рендеринг только свойств Name и ListPrice:
var grid = new WebGrid(Model, columnNames:
new[] {"Name", "ListPrice"});
Указать нужные поля можно и при вызове GetHtml, а не конструктора. Хотя код при этом получается длиннее, у вас появляется возможность передачи дополнительной информации о том, как выполнять рендеринг полей. В следующем примере через свойство header я делаю столбец ListPrice более дружественным к пользователю:
@grid.GetHtml(columns: grid.Columns(
grid.Column("Name"),
grid.Column("ListPrice", header:"List Price")
)
)
Очень часто при рендеринге списка элементов нужно давать возможность пользователям щелкать любой из элементов для перехода в представление Details. Параметр format метода Column позволяет настраивать рендеринг элемента данных. В следующем коде показано, как изменять рендеринг имен, чтобы вывести ссылку для какого-либо элемента в представлении Details; при этом выводится прайс-лист, в котором цены округляются с точностью до двух разрядов после точки, как видно на рис. 2.
@grid.GetHtml(columns: grid.Columns(
grid.Column("Name", format: @<text>@Html.ActionLink(
(string)item.Name, "Details", "Product",
new {id=item.ProductId}, null)</text>),
grid.Column("ListPrice", header:"List Price",
format: @<text>@item.ListPrice.ToString("0.00")
</text>)
)
)
Рис. 2. Базовая сетка с отформатированными полями
Параметр format на самом деле представляет собой Func<dynamic,object> — делегат, который принимает динамический параметр и возвращает объект. Механизм Razor принимает фрагмент, указанный в параметре format, и преобразует его в делегат. Этот делегат принимает динамический параметр с именованным элементом (item), и это переменная item, используемая в данном фрагменте format. Подробнее о том, как работают такие делегаты, см. публикацию Фила Хаака (Phil Haack) в блоге по ссылке bit.ly/h0Q0Oz.
Вызов методов расширения с динамическими параметрами не поддерживается.
Поскольку параметр item имеет тип dynamic, при написании кода вы лишаетесь поддержки IntelliSense или проверок со стороны компилятора (см. статью Александры Русины по динамическим типам в февральском номере по ссылке msdn.microsoft.com/ru-ru/magazine/gg598922). Более того, вызов методов расширения с динамическими параметрами не поддерживается. То есть при вызове методов расширения вы должны использовать статические типы — это и является причиной того, что item.Name приводится к строке, когда я вызываю метод расширения Html.ActionLink в предыдущем коде. В ряде методов расширения, применяемых в ASP.NET MVC, конфликт между dynamic и методами расширения может здорово досаждать (это тем более верно, если вы используете нечто вроде T4MVC — см. bit.ly/9GMoup).
Добавление строгой типизации
Хотя динамическая типизация, вероятно, является хорошим выбором для WebMatrix, свои преимущества есть и у представлений со строгой типизацией. Один из способов получить их — создать производный тип WebGrid<T> (рис. 3). Как видите, это весьма облегченная оболочка!
Рис. 3. Создание производного WebGrid
public class WebGrid<T> : WebGrid
{
public WebGrid(
IEnumerable<T> source = null,
...(список параметров опущен для краткости)
: base(
source.SafeCast<object>(),
...(список параметров опущен для краткости)
{ }
public WebGridColumn Column(
string columnName = null,
string header = null,
Func<T, object> format = null,
string style = null,
bool canSort = true)
{
Func<dynamic, object> wrappedFormat = null;
if (format != null)
{
wrappedFormat = o => format((T)o.Value);
}
WebGridColumn column = base.Column(
columnName, header,
wrappedFormat, style, canSort);
return column;
}
public WebGrid<T> Bind(
IEnumerable<T> source,
IEnumerable<string> columnNames = null,
bool autoSortAndPage = true,
int rowCount = -1)
{
base.Bind(
source.SafeCast<object>(),
columnNames,
autoSortAndPage,
rowCount);
return this;
}
}
public static class WebGridExtensions
{
public static WebGrid<T> Grid<T>(
this HtmlHelper htmlHelper,
...(список параметров опущен для краткости)
{
return new WebGrid<T>(
source,
...(список параметров опущен для краткости);
}
}
Что это нам дает? С помощью новой реализации WebGrid<T> я добавил новый метод Column, который принимает Func<T, object> в качестве параметра format, а значит, при вызове методов расширения преобразование типов не потребуется. Кроме того, вы теперь получаете поддержку IntelliSense и проверки компилятором (если в файле проекта включен MvcBuildViews; по умолчанию он отключен).
В ряде методов расширения, применяемых в ASP.NET MVC, конфликт между dynamic и методами расширения может здорово досаждать.
Метод расширения Grid позволяет задействовать преимущества логического распознавания типов компилятором применительно к обобщенным параметрам. Поэтому в данном примере можно написать Html.Grid(Model) вместо new WebGrid<Product>(Model). В любом случае возвращаемым типов является WebGrid<Product>.
Добавление поддержки разбиения на страницы и сортировки
Вы уже видели, что WebGrid предоставляет функциональность разбиения на страницы и сортировки безо всяких усилий с вашей стороны. Кроме того, вы узнали, как сконфигурировать страницу через параметр rowsPerPage (в конструкторе или вызовом вспомогательного метода Html.Grid), чтобы сетка автоматически показывала одну страницу данных и визуализировала элементы управления для навигации между страницами. Однако поведение по умолчанию может оказаться не тем, что вам хотелось бы. Чтобы проиллюстрировать это, я добавил код для рендеринга ряда элементов в источнике данных после рендеринга сетки, как показано на рис. 4.
Рис. 4. Несколько элементов в источнике данных
Как видите, передаваемые нами данные содержат полный список продуктов (295 в этом примере, но нетрудно вообразить ситуации, где извлекается куда больше данных). По мере увеличения объема возвращаемых данных вы все больше нагружаете свои сервисы и базы данных, но по-прежнему визуализируете только одну страницу данных. Однако есть подход получше: разбиение на страницы на серверной стороне (server-side paging). В этом случае вы извлекаете лишь те данные, которые нужны для отображения текущей страницы (например, лишь пяти записей).
Первый шаг в реализации для WebGrid разбиения на страницы на серверной стороне — ограничение данных, извлекаемых из источника. Для этого нужно знать, какая страница запрашивается; тогда можно извлечь правильную страницу. Выполняя рендеринг ссылок на страницы, WebGrid повторно использует URL страницы и включает в параметр строки запроса номера страницы, например http://localhost:27617/Product/DefaultPagingAndSorting?page=3 (имя параметра строки запроса настраивается через параметры вспомогательного метода — это удобно, если вы хотите поддерживать на странице разбиение более чем для одной сетки). То есть ваш метод операции может принимать параметр page, и этот параметр будет заполнен значением строки запроса.
Если вы просто измените существующий код, чтобы передавать одну страницу данных в WebGrid, то этот элемент увидит всего одну страницу данных. Поскольку ему не известно о наличии других страниц, он больше не будет визуализировать элементы управления для навигации между страницами. К счастью, в WebGrid есть другой метод, Bind, с помощью которого можно указывать данные. Bind принимает не только данные: у него есть параметр, через который вы передаете общее количество записей, позволяющее вычислять число страниц. Чтобы задействовать этот метод, операцию List нужно обновить так, чтобы она извлекала дополнительную информацию для передачи в представление (рис. 5).
Первый шаг в реализации для WebGrid разбиения на страницы на серверной стороне — ограничение данных, извлекаемых из источника.
Рис. 5. Обновление операции List
public ActionResult List(int page = 1)
{
const int pageSize = 5;
int totalRecords;
IEnumerable<Product> products = productService.GetProducts(
out totalRecords, pageSize:pageSize, pageIndex:page-1);
PagedProductsModel model = new PagedProductsModel
{
PageSize= pageSize,
PageNumber = page,
Products = products,
TotalRows = totalRecords
};
return View(model);
}
Благодаря этой дополнительной информации представление можно обновить для использования метода WebGrid.Bind. При вызове Bind передаются визуализируемые данные и общее количество записей, а также параметру autoSortAndPage присваивается значение false. Этот параметр указывает WebGrid, что разбивать на страницы не требуется, так как об этом позаботится метод-операция List. Для иллюстрации сказанного взгляните на следующий код:
<div>
@{
var grid = new WebGrid<Product>(null, rowsPerPage: Model.PageSize,
defaultSort:"Name");
grid.Bind(Model.Products, rowCount: Model.TotalRows, autoSortAndPage: false);
}
@grid.GetHtml(columns: grid.Columns(
grid.Column("Name", format: @<text>@Html.ActionLink(item.Name,
"Details", "Product", new { id = item.ProductId }, null)</text>),
grid.Column("ListPrice", header: "List Price",
format: @<text>@item.ListPrice.ToString("0.00")</text>)
)
)
</div>
После этих изменений WebGrid вернется к жизни, визуализируя элементы управления навигацией, но разбиение на страницы будет осуществляться сервисом, а не в представлении! Однако при выключенном параметре autoSortAndPage функциональность сортировки ломается. WebGrid использует параметры строк запросов для передачи сортируемых столбцов и направления сортировки, а мы указали ему не выполнять сортировку. Для устранения этой проблемы нужно добавить параметры sort и sortDir к методу операции и передавать их через сервис, чтобы тот выполнял необходимую сортировку, как показано на рис. 6.
Рис. 6. Добавление параметров сортировки к методу операции
public ActionResult List(
int page = 1,
string sort = "Name",
string sortDir = "Ascending" )
{
const int pageSize = 5;
int totalRecords;
IEnumerable<Product> products =
_productService.GetProducts(out totalRecords,
pageSize: pageSize,
pageIndex: page - 1,
sort:sort,
sortOrder:GetSortDirection(sortDir)
);
PagedProductsModel model = new PagedProductsModel
{
PageSize = pageSize,
PageNumber = page,
Products = products,
TotalRows = totalRecords
};
return View(model);
}
AJAX: изменения на клиентской стороне
WebGrid поддерживает асинхронное обновление содержимого сетки через AJAX. Чтобы задействовать это преимущество, вам нужно лишь убедиться, что у div, содержащего сетку, есть идентификатор, а затем передать этот идентификатор в параметре ajaxUpdateContainerId конструктору сетки. Вам также потребуется ссылка на jQuery, но она уже включена в представление разметки (layout view). Когда ajaxUpdateContainerId указан, WebGrid меняет свое поведение так, чтобы ссылки, относящиеся к разбиению страниц и сортировке, использовали AJAX для обновлений:
<div id="grid">
@{
var grid = new WebGrid<Product>(null, rowsPerPage: Model.PageSize,
defaultSort: "Name", ajaxUpdateContainerId: "grid");
grid.Bind(Model.Products, autoSortAndPage: false, rowCount: Model.TotalRows);
}
@grid.GetHtml(columns: grid.Columns(
grid.Column("Name", format: @<text>@Html.ActionLink(item.Name,
"Details", "Product", new { id = item.ProductId }, null)</text>),
grid.Column("ListPrice", header: "List Price",
format: @<text>@item.ListPrice.ToString("0.00")</text>)
)
)
</div>
Хотя встроенная функциональность для использования AJAX весьма хороша, генерация вывода не будет работать при отключении поддержки скриптов. Причина этого в том, что в режиме AJAX компонент WebGrid осуществляет рендеринг анкерных тегов (anchor tags) с href, равным «#», и вводит поведение AJAX через обработчик onclick.
Я всегда стараюсь создавать страницы, функциональность которых корректно сокращается при отключении поддержки скриптов, и, в целом, считаю, что лучший способ добиться этого — применять пропорциональное расширение (т. е. страница работает без поддержки скриптов, а при добавлении этой поддержки функциональность страницы просто расширяется). С этой целью вы можете прибегнуть к WebGrid без AJAX и создать скрипт, показанный на рис.7, для повторного встраивания поведения AJAX.
Рис. 7. Повторное встраивание поведения AJAX
$(document).ready(function () {
function updateGrid(e) {
e.preventDefault();
var url = $(this).attr('href');
var grid = $(this).parents('.ajaxGrid');
var id = grid.attr('id');
grid.load(url + ' #' + id);
};
$('.ajaxGrid table thead tr a').live('click', updateGrid);
$('.ajaxGrid table tfoot tr a').live('click', updateGrid);
});
Чтобы разрешить применение скрипта только к WebGrid, в этом коде используются jQuery-селекторы, которые идентифицируют элементы с установленным классом ajaxGrid. Этот скрипт устанавливает обработчики события щелчка для ссылок сортировки и разбиения на страницы (они идентифицируются через верхний и нижний заголовки таблицы внутри контейнера сетки), используя jQuery-метод live (api.jquery.com/live). Он устанавливает обработчик событий для существующих и будущих элементов, отвечающих критериям селектора, что удобно, если скрипт будет заменять содержимое.
Метод updateGrid устанавливается как обработчик событий и первое, что он делает, — вызывает preventDefault, чтобы отключить поведение по умолчанию. После этого он получает URL для использования (из атрибута href анкерного тега), а затем выдает AJAX-вызов для загрузки обновленного содержимого в элемент-контейнер. Чтобы применить этот подход, убедитесь, что AJAX-поведение по умолчанию в WebGrid отключено, добавьте класс ajaxGrid в контейнер div и вставьте скрипт с рис. 7.
AJAX: изменения на серверной стороне
Обратите внимание на то, что скрипт использует функциональность jQuery-метода load для изоляции фрагмента от возвращаемого документа. Простой вызов load(‘http://example.com/someurl’) загрузит содержимое, находящееся по данному URL, а вызов load(‘http://example.com/someurl #someId’) будет не только загружать содержимое, но и возвращать фрагмент с идентификатором «someId». Это дублирует AJAX-поведение WebGrid по умолчанию и избавляет вас от обновления серверного кода для добавления поддержки рендеринга части данных; WebGrid будет загружать полную страницу, а затем убирать из нее новую сетку.
В плане быстрого получения AJAX-функциональности это просто отлично, но подразумевает, что вы передаете по сети больше данных, чем необходимо, и потенциально перебираете на сервере больше данных, чем могли бы. К счастью, ASP.NET MVC позволяет весьма легко справиться с этими проблемами. Основная идея в том, чтобы выделить весь рендеринг, общий для запросов с AJAX и без в частичное представление (partial view). Тогда операция List в контроллере сможет выполнять рендеринг либо только частичного представления для вызовов с AJAX, либо полного представления (которое в свою очередь использует частичное представление) для вызовов без AJAX.
Этот подход может оказаться предельно простым, и вам будет достаточно проверять результат метода расширения Request.IsAjaxRequest, вызываемого внутри вашего метода операции. Все будет работать великолепно, если пути кода для AJAX и без AJAX имеют лишь незначительные различия. Однако зачастую эти пути различаются куда значимее (например, при полном рендеринге требуется больше данных, чем при частичном). В этой ситуации вы, вероятно, написали бы AjaxAttribute, чтобы можно было создавать раздельные методы, а затем предоставили бы инфраструктуре MVC выбор нужного метода в зависимости от запроса (с AJAX или без) — точно так же, как работают атрибуты HttpGet и HttpPost. Соответствующий пример см. в моем блоге по ссылке bit.ly/eMlIxU.
WebGrid и механизм представлений Web Forms
До сих пор во всех примерах использовался механизм представлений Razor. В простейшем случае вам не придется что-либо менять для использования WebGrid с механизмом представлений Web Forms (не считая различий в синтаксисе механизма представлений). В предыдущих примерах я показал, как можно настроить рендеринг данных из записей, используя параметр format:
grid.Column("Name",
format: @<text>@Html.ActionLink((string)item.Name,
"Details", "Product", new { id = item.ProductId }, null)</text>),
Параметр format на самом деле является делегатом Func, но механизм представлений Razor скрывает это от нас. Но вы вправе сами передавать Func — например, вы могли бы использовать лямбда-выражение:
grid.Column("Name",
format: item => Html.ActionLink((string)item.Name,
"Details", "Product", new { id = item.ProductId }, null)),
И теперь вы можете легко задействовать преимущества WebGrid с механизмом представлений Web Forms!
WebGrid поддерживает асинхронное обновление содержимого сетки через AJAX.
Заключение
В этой статье я показал, как с помощью нескольких весьма простых приемов использовать преимущества функциональности WebGrid, не жертвуя строгой типизацией, поддержкой IntelliSense или эффективным разбиением на страницы на серверной стороне. В WebGrid встроена весьма впечатляющая функциональность, которая помогает вам эффективно решать задачи рендеринга табличных данных. Надеюсь, что эта статья позволила вам прочувствовать, как выжать максимум возможного из этого компонента в приложении ASP.NET MVC.