Показаны сообщения с ярлыком MVC. Показать все сообщения
Показаны сообщения с ярлыком MVC. Показать все сообщения

пятница, 27 августа 2010 г.

Создание GridView-контрола в WebForms и MVC с SEO-friendly пейжингом

SEO-friendly пейжинг в ASP.NET webForms

В предыдущем посте я описывал разные способы создания аналога контрола <asp:GridView /> средствами ASP.NET MVC. В итоге полученные результаты хоть и были более трудоемкими в создании кода, но обладали некоторыми свойствами, недоступными привычному WebForms контролу, такими как, например: загрузка данных через AJAX-запрос, более удобное назначение стилей элементов таблицы, контроль над генерируемым кодом.

Конечно, рассмотрев несколько удачных примеров замены серверного контрола в среде WebForms на аналогичные (и даже более богатые возможностями) контролы в среде MVC, мы с ухмылкой посматриваем на старый проверенный элемент разметки страницы <asp:GridView runat=”server”… />, подбирая в уме замену более подходящим вариантом. Но не стоит спешить выделять блок страницы и жать кнопку <del> – смогут ли решить следующую задачу найденные нами jQuery-контролы, с которой старый добрый GridView опять справляется на 5 баллов.

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

Вроде бы ничего нового. Но… Что значит - Добавить возможность перехода по пейжингу поисковым роботам?

Посещая ресурс, поисковый робот загружает только тот HTML-код, который был возвращен на запрос к сайту. Если мы используем jQuery-плагин, который загружает содержимое таблицы после загрузки основной страницы дополнительным AJAX-запросом через JavaScript-функцию, то поисковый робот получит HTML-код с пустой таблицей.

image

Если же мы используем плагин, который осуществляет переход по страницам таблицы посредством вызова JavaScript-функции – поисковый робот опять-таки далее загрузки основной таблицы не пойдет.

Почему? Ответ простой – поисковые роботы не выполняют .js-код при загрузке содержимого страницы.

Но тут же и на время стопорится работа и при использовании привычного WebForms контрола <asp:GridView  />, так как для перехода по страницам пейжинга он использует postback-механизм (нажатие кнопки для осуществления перехода на следующую страницу), что нарушает еще одно правило поведения робота на странице: поисковый робот на нежимает на кнопки.

image

Попробуйте загрузить любую страницу интернета с помощью Lynx-браузера и увидите ее так, как видит ее поисковый бот. Вот пример страницы Торговой площадки Ydobno, отображенной с помощью lynx-браузера.

image

Как видим – никаких стилей, никаких скриптов. Чистый текст. Отдльно от всего текста браузер выделяет список ссылок, по которым поисковой робот в дальнейшем может переходить.

image

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

image

Хорошо, проблема ясна. Теперь можно искать ее решение. Для начала конечно же эксперементируем с нашим серверным контролом. Как упоминалось в самом начале об этом контроле – что нам снег, что нам зной, что нам пейжинг сотворить. Но, конечно, не без дополнительных манипуляций.

В паре с серверным контролом используем еще один контрол - <asp:DataPager runat=”server” />, который как раз и позволяет нам генерировать пейжинг в одном из следующих форматов: числовое отображение количества страниц (1-10, 11-20 и т.д.), переход вперед-назад, custom template пейжинг. При этом мы можем объединять варианты, получая числовые ссылки и ссылки вперед-назад по краям.

Итак, добавляем на страницу следующий контрол:

<asp:DataPager ID="DataPager1" runat="server" QueryStringField="page" PagedControlID="gvEmployee">
<Fields>
<asp:NextPreviousPagerField FirstPageText="|&lt;&lt;" ShowFirstPageButton="False" ShowNextPageButton="False" ShowPreviousPageButton="True" PreviousPageText="&lt;&lt;" />
<asp:NumericPagerField ButtonCount="10" CurrentPageLabelCssClass="dataPager-current" />
<asp:NextPreviousPagerField LastPageText="&gt;&gt;|" ShowLastPageButton="False" ShowPreviousPageButton="False" ShowNextPageButton="True" NextPageText="&gt;&gt;" />
</Fields>
</asp:DataPager>



Как мы видим, данный контрол имеет свойство PagedControlID, которому присваиваем ID нашего грида для построения пейжинга. Одна лишь проблема, если мы запустим данный код на выполнение, то получим следующую страницу:


image


Серверный контрол <asp:GridView …/> не имплементирует интерфейс IPageableItemContainer. Для решения данной проблемы мы создаем новый класс DataPagerGridView, унаследованный от GridView, имплементирующий необходимый интерфейс:


/// <summary>
/// DataPagerGridView is a custom control that implements GrieView and IPageableItemContainer
/// </summary>
public class DataPagerGridView : GridView, IPageableItemContainer
{
public DataPagerGridView()
: base()
{
PagerSettings.Visible = false;
}

/// <summary>
/// TotalRowCountAvailable event key
/// </summary>
private static readonly object EventTotalRowCountAvailable = new object();

/// <summary>
/// Call base control's CreateChildControls method and determine the number of rows in the source
/// then fire off the event with the derived data and then we return the original result.
/// </summary>
/// <param name="dataSource"></param>
/// <param name="dataBinding"></param>
/// <returns></returns>
protected override int CreateChildControls(IEnumerable dataSource, bool dataBinding)
{
int rows = base.CreateChildControls(dataSource, dataBinding);

// if the paging feature is enabled, determine the total number of rows in the datasource
if (this.AllowPaging)
{
// if we are databinding, use the number of rows that were created, otherwise cast the datasource to an Collection and use that as the count
int totalRowCount = dataBinding ? rows : ((ICollection)dataSource).Count;

// raise the row count available event
IPageableItemContainer pageableItemContainer = this as IPageableItemContainer;
this.OnTotalRowCountAvailable(new PageEventArgs(pageableItemContainer.StartRowIndex, pageableItemContainer.MaximumRows, totalRowCount));

// make sure the top and bottom pager rows are not visible
if (this.TopPagerRow != null)
this.TopPagerRow.Visible = false;

if (this.BottomPagerRow != null)
this.BottomPagerRow.Visible = false;
}
return rows;
}

/// <summary>
/// Set the control with appropriate parameters and bind to right chunk of data.
/// </summary>
/// <param name="startRowIndex"></param>
/// <param name="maximumRows"></param>
/// <param name="databind"></param>
void IPageableItemContainer.SetPageProperties(int startRowIndex, int maximumRows, bool databind)
{
int newPageIndex = (startRowIndex / maximumRows);
this.PageSize = maximumRows;

if (this.PageIndex != newPageIndex)
{
bool isCanceled = false;
if (databind)
{
// create the event arguments and raise the event
GridViewPageEventArgs args = new GridViewPageEventArgs(newPageIndex);
this.OnPageIndexChanging(args);

isCanceled = args.Cancel;
newPageIndex = args.NewPageIndex;
}

// if the event wasn't cancelled change the paging values
if (!isCanceled)
{
this.PageIndex = newPageIndex;

if (databind)
this.OnPageIndexChanged(EventArgs.Empty);
}
if (databind)
this.RequiresDataBinding = true;
}
}

/// <summary>
/// IPageableItemContainer's StartRowIndex = PageSize * PageIndex properties
/// </summary>
int IPageableItemContainer.StartRowIndex
{
get { return this.PageSize * this.PageIndex; }
}

/// <summary>
/// IPageableItemContainer's MaximumRows = PageSize property
/// </summary>
int IPageableItemContainer.MaximumRows
{
get { return this.PageSize; }
}

/// <summary>
///
/// </summary>
event EventHandler<PageEventArgs> IPageableItemContainer.TotalRowCountAvailable
{
add { base.Events.AddHandler(DataPagerGridView.EventTotalRowCountAvailable, value); }
remove { base.Events.RemoveHandler(DataPagerGridView.EventTotalRowCountAvailable, value); }
}

/// <summary>
///
/// </summary>
/// <param name="e"></param>
protected virtual void OnTotalRowCountAvailable(PageEventArgs e)
{
EventHandler<PageEventArgs> handler = (EventHandler<PageEventArgs>)base.Events[DataPagerGridView.EventTotalRowCountAvailable];
if (handler != null)
{
handler(this, e);
}
}
}


Готово. Теперь можно добавлять custom-контрол на страницу, связывать с источником данных и с пейжинг-контролом:


<%@ Register TagPrefix="pager" Namespace="WebApplication2" Assembly="WebApplication2" %>
...
<pager:DataPagerGridView ID="gvEmployee" runat="server" DataSourceID="dsEmployee" AllowPaging="true" />
<asp:DataPager ID="DataPager1" runat="server" QueryStringField="page" PagedControlID="gvEmployee">
<Fields>
<asp:NextPreviousPagerField FirstPageText="|&lt;&lt;" ShowFirstPageButton="False" ShowNextPageButton="False" ShowPreviousPageButton="True" PreviousPageText="&lt;&lt;" />
<asp:NumericPagerField ButtonCount="10" CurrentPageLabelCssClass="dataPager-current" />
<asp:NextPreviousPagerField LastPageText="&gt;&gt;|" ShowLastPageButton="False" ShowPreviousPageButton="False" ShowNextPageButton="True" NextPageText="&gt;&gt;" />
</Fields>
</asp:DataPager>


Судя по декларации DataPager-контрола имеем ссылку на предыдущую страницу в начале и на следующую страницу в конце пейжинга, а также имеет ссылки на 10 текущих страниц (поскольку у нас мало данных, то показано всего 2 страницы).


Вуаля! Получаем следующий результат:


image


Отлично! Полученный результат вполне удовлетворяет нашим пожеланием относительно возможности максимально глубокого сканирования поисковым ботом содержимого сайта. И, опять таки, решение построение на базе мощного серверного контрола <asp:GridView /> при помощи <asp:DataPager />. Теперь, загружая данную страницу, поисковый бот получит и список ссылок на другие страницы пейжинга, что позволит ему последовательно посетить и просканировать все элементы данной таблицы.


 


SEO-friendly пейжинг в ASP.NET MVC


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


Поскольку конечным результатом нам необходимо отобразить запрашиваемый срез данных и показать пейжинг для перехода к другим страницам контрола, то для более удобной манипуляции данными на сервере было бы хорошо использовать объект, позволюящий нам хранить лишь нужный нам в данный момент набор данных, но при этом сохраняя информацию об общем количестве объектов, как далеко мы находимся от начала набора и т.д. В процессе приготовления к данному материалу я наткнулся на реализацию данного класса на codeplex - PagedList<T> (либо же можно найти похожую реализацию на этом блоге). Для работы с данными в веб-ориентированных проектах данный класс будет крайне пригодным, так как позволяет превратить любой объект, наследуемый от IEnumerable, в объект PagedList, благодаря которому можно получить много полезной информации о наборе данных: первая ли это страница из всего набора, последняя ли это страница, общее количество данных, количество данных в отображаемой наборе и т.д.


Для отображения данных на страницу создадим отдельный HtmlHelper-метод – GridView.


public static class GridViewExtensions
{
public static void GridView<T>(
this HtmlHelper html,
PagedList<T> data,
Action<PagedList<T>> headerTemplate,
Action<T, string> itemTemplate,
string cssClass,
string cssAlternatingClass,
Action<PagedList<T>> footerTemplate,
Action<PagedList<T>> paging)
{
headerTemplate(data);

for (int i = 0; i < data.Count; i++)
{
var item = data[i];
itemTemplate(item, (i % 2 == 0 ? cssClass : cssAlternatingClass));
}

footerTemplate(data);
paging(data);
}
}


Чтобы жестко не прописывать логику генерации кода контрола, а сделать его универсальным для отображения разных табличных данных – прибегаем к использованию Action-методов (чтобы не загружать наш контрол делегатными функциями), логику работы которых мы будем индивидуально прописывать в шаблоне View. Конечно это заставит нас писать больше кода для каждого View, в котором мы будем использовать данный контрол, поэтому для полного комфорта было бы хорошо ввести два контрола – один с широкими возможностями для изменения внешнего вида, второй – построение единого шаблона отображения, но и требующего написания меньше кода. Остановимся пока на первом варианте.


Создаем новый strongly-typed view и вводим следующий тип (он не будет присутствовать в выпадающем списке) -PagedList<EmployeeModel>


image



Добавляем следующий код на страницу:


<% Html.GridView<MvcApplication1.Models.EmployeeModel>(
this.ViewData.Model,
data =>
{ %>
<table class="grid" cellpadding="0" cellspacing="0">
<% },
(item, css) =>
{ %>
<tr class="<%=css%>">
<td><%=item.EmployeeID%></td>
<td><%=item.FirstName%></td>
<td><%=item.LastName%></td>
</tr>
<% },
"item",
"item-alternating",
data =>
{ %>
</table>
<% },
data =>
{ %>
<%= Html.Pager(Model, "/Employee/GetList")%>
<%}); %>



Необходимо разобрать более детально этот код для лучшего его понимания.

Для генерации табличного контрола мы создали статический html-helper метод GridView, в который в нужной последовательности необходимо передать данные, согласно декларации метода. Хотелось бы сильнее обратить внимание на следующую запись:


data =>
{ %>
<table class="grid" cellpadding="0" cellspacing="0">
<% },





согласно декларации метода в классе третьим параметором мы должны были передать Action-метод

...
Action<PagedList<T>> headerTemplate
...



Как Action-метод может быть заменен такой конструкцией? Сначала необходимо разобраться что из себя представляет класс Action и как серверный код превращается в html-код.


Начнем со второго вопроса. Не задумывались ла Вы, почему компилятор при компиляции страницы указывает лишь об ошибках внутри серверных блоков (<% %>, <%= %>), но пропускает любое упоминание об ошибке в записи Html-тега например? Все очень просто: для компилятора все что внутри серверных блоков – это инструкции для выполнения, все что за пределами – это строки текста, которые просто надо вывести на страницу. Представим что мы имеем следующую декларацию на странице:


<ul>
<% for(int i=1; i<=3; i++) { %>
<li><%= i %></li>
<% } %>
</ul>

При компиляции страницы в сборку данный код будет выглядеть приблизительно так:


Response.Write("<ul>");
for(int i=1; i<=3; i++)
{
Response.Write("<li>");
Response.Write(i);
Response.Write("</li>");
}
Response.Write("</ul>");


Именно поэтому компилятор не показывает сообщение об ошибке записи тега, как как для него – это обычный блок текста, который необходимо вывести в output-поток согласно своей позиции.


Что же такое Action-методы? Объекты данного класса используются для замены делегатов-функций определенного синтаксиса – принимают один параметр и на выходе не возвращают ничего (void). К тому же Action-методы позволяют задавать декларации с использованием анонимных функций и лямбда-выражений. Следующий код демонстрирует простой пример использования Action-метода для замены делегатов.


public static void Main()
{
Action<string> messageTarget;
messageTarget = s => ShowWindowsMessage(s);

messageTarget("Hello, World!");
}

private static void ShowWindowsMessage(string message)
{
MessageBox.Show(message);
}


В нашем же примере мы используем тоже анонимные функции и лямбда-выражение для декларации Action-метода.

В итоге, наш код


data =>
{ %>
<table class="grid" cellpadding="0" cellspacing="0">
<% },


можно превратить в следующую аналогичную запись:





delegate(PagedList<T> data) { Response.Write("<table class='grid' cellpadding='0' cellspacing='0'>"); },


Хорошо,  детально разобрали декларацию нового HtmlHelper-метода GridView. Но, как видно, для отображения пейжинга на странице используем еще один HtmlHelper-метод – Pager.


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


После компиляции мы получил следующий результат:


image


Если просмотреть html-код, то видим, что весь контент присутствует и ссылки на месте. Это значит, что при загрузке данной страницы поисковым роботом, он получит всю информацию для индексации.


Отлично, поставленная задача опять выполнена, хотя и потребовалось много дополнительной ручной работы – создать дополнительные контролы и дополнительные классы для оперирования данными. Но при этом опять с лучшим результатом, чем для серверного контрола <asp:GridView runat=”server” />, а именно получили ссылку без параметров строки (article.aspx?Id=2, как в примере с WebForms), а полноценную ссылку. В этом случае поисковики больше доверяют ссылке без параметров строки.


Для упрощения работы с созданием табличных контролов можно посоветовать использовать контролы сторонних разработчиков. Для примера компания Telerik предлагает удобный Grid-контрол для MVC.


image


Примеры его использования можно посмотреть здесь.

пятница, 30 июля 2010 г.

GridView в MVC - jQuery плагины для манипуляции с табличными данными

Не секрет, что важным правилом для любого уважающего себя веб-мастера является - избегать использование тегов <table></table> для построения интерфейса пользователя. В то же время нет более подходящего набора элементов, чем <table>,<th>, <tr>, <td> для структурирования и удобного отображения табличных данных.

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

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

Для сравнения возмем привычный серверный компонент для ASP.NET WebForms - <asp:GridView>, позволяющий очень просто отобразить табличные данные на странице.

   1: <asp:GridView ID="gvEmployee" runat="server" DataSourceID="dsEmployee" />
   2:  
   3: <asp:SqlDataSource ID="dsEmployee" runat="server" 
   4:     ConnectionString="<%$ ConnectionStrings:NorthwindConnectionString %>" 
   5:     SelectCommand="SELECT [EmployeeID], [FirstName], [LastName] FROM [Employees]">
   6: </asp:SqlDataSource>

В результате получим следующий контент на странице:


image


За простой записью декларации скрываются сложные механизмы рендеринга контента, получения данных из источника данных, привязки данных к контролу для последующего отображения. Прелесть WebForms для начинающего программиста в том, что сложное может создаваться простой декларацией на странице. Сложные механизмы авторизации можно добавлять к сайту, не вникая в детали их работы; отображение вложенности страниц легко создается простой декларацией .sitemap-файла и многое другое. Для ASP.NET MVC многие тривиальные задачи для WebForms могут превратится в сложные нетривиальные задачи, требующие глубокого освоения клиентских и серверных языков.


На примере все же данной статьи хотелось бы проверить – насколько сложно будет повтрорить огромную функциональность серверного контрола <asp:GridView />в рамках модели MVC.


Хорошо, просто отобразить табличные данные подобно примеру выше не составляет особого труда – выводим их на страницу с помощью foreach-перебора.



<table>
    <tr>
        <th>EmployeeID</th>
        <th>FirstName</th>
        <th>LastName</th>
    </tr>
 
<% foreach (var item in Model) { %>
    <tr>
        <td><%= Html.Encode(item.EmployeeID) %></td>
        <td><%= Html.Encode(item.FirstName) %></td>
        <td><%= Html.Encode(item.LastName) %></td>
    </tr>
<% } %>
 
</table>

Получаем похожий результат:


image


Вполне приемлемый результат. К тому же в этом примере мы имеем полный контроль над генерируемым кодом, так как сами определяем структуру таблицы. Тем самым задаем стили отображения таблицы просто, без особого штудирования MSDN (как в примере с <asp:GridView />)


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


Привычный контрол <asp:GridView> справляется с этой задачей на все 100% без особых усилий – добавляем лишь пару атрибутов:



<asp:GridView ID="gvEmployee" runat="server" DataSourceID="dsEmployee" AllowPaging="true" PageSize="5" />

Результат:


image


MVC-модель в свою очередь заствляет разработчика остановится на данном этапе, откинуться на спинку кресла и предаться размышлениям. Увы, столь простого механизма нету. Делаем чай, ходим по комнате, внимательно протирая глазами дырку в полу – таки да, столь простого механизма нету.


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


DataTables – плагин для библиотеки jQuery. Чем особенно интересен плагин, так это возможностью делать гибкую таблицу с пейжингом, сортировками и т.д. из уже сгенерированного кода. Точнее – сначала мы генерируем таблицу, а потом используем плагин для придания этой таблице необходимых нам свойств.


Попробуем на примере.


Подключаем .js библиотеки:


<script src="../../Scripts/jquery-1.4.1.min.js" type="text/javascript"></script>
<script src="../../Scripts/jquery.dataTables.min.js" type="text/javascript"></script>


Создаем таблицу:


<table id="tblEmployees">
<thead>
<tr>
<th>EmployeeID</th>
<th>FirstName</th>
<th>LastName</th>
</tr>
</thead>
<tbody>
<% foreach (var item in Model) { %>
<tr>
<td><%= Html.Encode(item.EmployeeID) %></td>
<td><%= Html.Encode(item.FirstName) %></td>
<td><%= Html.Encode(item.LastName) %></td>
</tr>
<% } %>
</tbody>
</table>


Обязательно необходимо полно описывать структуру данных, используя теги <thead>, <tbody> – данный плагин использует их для разбиения на заголовок и контент. Пропустив теги <thead> и <tbody>, мы получим ошибку выполнения функции скрипта.


Добавляем следущий код на страницу:


<script type="text/javascript">
$(document).ready(function () {
$("#tblEmployees").dataTable();
});
</script>



Немного корректируем стили отображения элементов:


<style type="text/css">
#tblEmployees_wrapper { border:solid 2px #a4a4a4; width:450px }
#tblEmployees_length { float:left; padding:5px 30px 5px 5px }
#tblEmployees_filter { padding:5px; background-color:#d4d4d4 }
#tblEmployees_info { padding:5px 30px 5px 5px; background-color:#d4d4d4; float:left; font-size:10px }
#tblEmployees_paginate { padding:5px; background-color:#d4d4d4 }
#tblEmployees_paginate span { padding:2px; font-size:11px; color:#474d7c }
#tblEmployees_paginate span.paginate_button { cursor:pointer }
#tblEmployees_paginate span.paginate_button:hover { text-decoration:underline }
#tblEmployees_paginate span.paginate_active { font-weight:bold }
</style>


И получаем следующий результат:


image



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


TableSorter – также плагин к популярной JavaScript-библиотеки jQuery, который превращает стандартную таблицу с тегами THEAD и TBODY в удобный контрол с возможностью сортировки по колонкам без перезагрузки страницы.


Пробуем на примере:


Подключаем необходимые библиотеки



   1: <script src="../../Scripts/jquery-1.4.1.min.js" type="text/javascript"></script>
   2: <script src="../../Scripts/jquery.tablesorter.min.js" type="text/javascript"></script>
   3: <script src="../../Scripts/jquery.tablesorter.pager.js" type="text/javascript"></script>


Последняя библиотека необходима для придания нашей таблице возможностей пейжинга.


Генерируем таблицу предыдущим способом:



   1: <table id="tblEmployees">
   2:     <thead>
   3:         <tr>
   4:             <th>EmployeeID</th>
   5:             <th>FirstName</th>
   6:             <th>LastName</th>
   7:         </tr>
   8:     </thead>
   9:     <tbody>
  10:         <% foreach (var item in Model) { %>
  11:             <tr>
  12:                 <td><%= Html.Encode(item.EmployeeID) %></td>
  13:                 <td><%= Html.Encode(item.FirstName) %></td>
  14:                 <td><%= Html.Encode(item.LastName) %></td>
  15:             </tr>
  16:         <% } %>
  17:     </tbody>
  18: </table>


Добавляем следующий код на страницу:



   1: $(function () {
   2:     $("#tblEmployees").tablesorter({ widthFixed: true }).tablesorterPager({ container: $("#pager") });
   3: });


и изменяем стиль отображения контрола:



   1: #tblEmployees th { text-align: left; padding: 5px; background-color: #6E6E6E; color:#ffffcc; cursor:pointer }
   2: #tblEmployees td { color: #FFF; padding: 5px; }
   3: #tblEmployees { font-size: 12px; background-color: #4D4D4D; border: 1px solid #000; }
   4: #pager .pagedisplay { width:50px }


На выходе получаем следующий результат:


image


Как видим – полученный результат вполне удовлетворяет нашим потребностям: пейжинг, сортировка по колонкам.


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


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


image


Ingrid – еще один интересный jQuery плугин для придания таблицам свойств: сортировка, пейжниг, назначение стилей таблицам, колонкам, подгрузка данных с помощью AJAX-запросов и многое другое.


image


TablePagination – очень простой jQuery плагин, добавляющий возможности пейжинга к существующей таблице.


image


Итак, судя по анализу, существует несколько способов создать MVC-контрол, аналогичный WebForms контролу – asp:GridView. Но не полноценный аналог, а лишь повторяющий некоторую функциональность данного контрола – сортировка по столбцам, пейжинг.


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


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


<asp:ObjectDataSource ID="odsEmployee" runat="server" SelectMethod="GetAll" SelectCountMethod="GetTotalCount" TypeName="Employee" />



и реализовав 2 метода для получения теущего набора данных и общего количесва данных:



public static List<Employee> GetAll()
{
...
}

public static int GetTotalCount()
{
...
}


можно подгружать только отображаемые в данных момент данные, а переходя по страницам пейжинга, подгружать новые.


Попробуем создать подобный контрол для ASP.NET MVC. Задача – загружать только те данные, которые пользователь просматривает в данный момент и подгружать по мере необходимости новые.


Сканируя документацию по вышеперчесленным jQuery-плагинам, находим следующие полезные возможности:


DataTables – возможность задавать конструктору локальный набор данных в виде JSON-объекта, либо URL – который возвращает JSON-объект. Используем данный плагин для создания контрола, отображающего только запрашиваемые в данный момент данные, не подгружая при этом ничего лишнего.


Редактируем существующий javascript-код на странице:


<script type="text/javascript">
   1:  
   2:     $(document).ready(function () {
   3:         $("#tblEmployees").dataTable({ "sPaginationType": "full_numbers",
   4:             "bProcessing": true,
   5:             "bServerSide": true,
   6:             "sAjaxSource": "../Employee/GetAllEmployees"
   7:         });
   8:     });
</script>


В конструкторе плагина мы указываем, что собираемся получать данные для построения таблицы из сервера и задаем URI для получения JSON-объекта.


В контроллере Employee создаем новый метод – GetAllEmployees:


public JsonResult GetAllEmployees()
{
//загружаем всех Employee. Если хотим совсем правильно делать, то создаем процедуру, возвращающую
//только запрашиваемый в данный момент набор
var data = EmployeeModel.GetAll();

//получаем текущее состояние пейжинга
var start = int.Parse(Request.QueryString["iDisplayStart"]);

//количество отображаемых элементов на странице
var pageCount = int.Parse(Request.QueryString["iDisplayLength"]);

var json = new { iTotalRecords = pageCount, iTotalDisplayRecords = data.Count, aaData = (from d in data.Skip(start).Take(pageCount) select new string[] { d.EmployeeID.ToString(), d.FirstName, d.LastName }) };
return Json(json, JsonRequestBehavior.AllowGet);
}


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


image


после чего генерируется привычная нам таблица данных:


image


Сперва я пытался сделать, чтобы URI для получения данных для плагина был ссылкой на веб-метод, но никак не мог добится того, чтобы объект получился в JSON-формате. Даже задавая атрибут [ScriptMethod(ResponseFormat=ResponseFormat.Json)] в результате получал XML-файл. Возможно кто-то подскажет мне – в чем я недоглядел :).



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


Итак, добавляем в проект нужные библиотеки:


<script src="../../Scripts/jquery-1.4.1.min.js" type="text/javascript"></script>
<script src="../../Scripts/flexigrid.js" type="text/javascript"></script>
<link href="../../Content/flexigrid.css" rel="stylesheet" type="text/css" />



Помещаем следующий код на страницу:


<table id="tblEmployees"></table>
<script type="text/javascript">
   1:  
   2:  
   3:     $("#tblEmployees").flexigrid({
   4:         url: 'GetFlexigridData',
   5:         dataType: 'json',
   6:         method: 'GET',
   7:         colModel: [
   8:             { display: 'ID', name: 'EmployeeID', width: 40, sortable: true, align: 'center' },
   9:             { display: 'First Name', name: 'FirstName', width: 150, sortable: true, align: 'left' },
  10:             { display: 'Last Name', name: 'LastName', width: 150, sortable: true, align: 'left'}],
  11:         sortname: "id",
  12:         sortorder: "asc",
  13:         usepager: true,
  14:         title: 'Employee List',
  15:         useRp: true,
  16:         rp: 10,
  17:         showTableToggleBtn: true,
  18:         height: 250,
  19:         width: 400
  20:     }
  21:     );
  22:  
</script>


Для получения JSON-данных содержимого таблицы создаем еще один метод в нашем Employee-контроллере, так как структура данных будет отлична от предыдущего примера:


public JsonResult GetFlexigridData()
{
var data = EmployeeModel.GetAll();
var start = int.Parse(Request.QueryString["page"]);
var pageCount = int.Parse(Request.QueryString["rp"]);

var json = new { page = start, total = data.Count, rows = (from d in data.Skip((start - 1) * pageCount).Take(pageCount) select new { id = d.EmployeeID, cell = new string[] { d.EmployeeID.ToString(), d.FirstName, d.LastName } }) };
return Json(json, JsonRequestBehavior.AllowGet);
}


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


image


Итак. Задачей данной статьи было найти примемленые JavaScript-плагины, которые в полной мере заменили бы мне до боли привычный и полностью освоенный контрол <asp:GridView>. Благодаря расширенному jQuery community довольно быстро были найдены и довольно быстро осовены разные плагины. К тому же я имел больше контроля над генерируемым кодом, а значит и больше контроля над присвоением стилей отображения данным контролам. К тому же подгрузка данных производилась с помощью AJAX-запросов, в то время как в обычном <asp:GridView /> я получал постоянно полноценный PostBack, а значит и полный цикл генерации страницы на сервере и передачи ее клиенту.

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

Читайте также о том, как сделать не просто пейжинг для контрола, а сделать его SEO-friendly, чтобы страницы грида могли также загружать роботы поисковиков, индексуруя старый контент. Нынешняя же реализация, к сожалению, этого не позволяет.