26 января 2013 г.

Создание глобальных расширений (helpers) в ASP.Net MVC, Часть 2

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

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

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

Сегодня мы создадим хэлпер для отображения на странице таблицы с какими-либо данными. Такой хэлпер должен уметь строить таблицы для любых обьектов, изменять количество столбцов в зависимости от настроек, а также для вывода данных использовать стороннюю разметку.
Итак, немного углубимся в задачу...
В нашем проекте есть две сущности: Person и Product. Каждый обьект содержит ряд свойств. Также, под каждую сущность создано частичное представление со строгой типизацией. Такое представление рисует разметку - данные об одном товаре/персоне. В проекте эти частичные расширения лежат в /Views/Home/, и называются: _productBox и _personBox.

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

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

Расширение будет называться DataList, и для его построения воспользуемся классом StringBuilder.

Давайте сначала посмотрим на весь хэлпер целиком, а потом уже узнаем, что там, и как:

   1:  public static class MyHelpers
   2:  {
   3:      public static MvcHtmlString DataList<T>(this HtmlHelper helper, 
                                           IEnumerable<T> items, int columns,
   4:          Func<T, HelperResult> template)
   5:          where T : class
   6:      {
   7:          if (items == null)
   8:              return MvcHtmlString.Create("");
   9:   
  10:          var sb = new StringBuilder();
  11:          sb.Append("<table style=\"width:100%;\">");
  12:   
  13:          int cellIndex = 0;
  14:   
  15:          foreach (T item in items)
  16:          {
  17:              if (cellIndex == 0)
  18:                  sb.Append("<tr>");
  19:   
  20:              sb.Append("<td>");
  21:   
  22:              sb.Append(template(item).ToHtmlString());
  23:              sb.Append("</td>");
  24:   
  25:              cellIndex++;
  26:   
  27:              if (cellIndex == columns)
  28:              {
  29:                  cellIndex = 0;
  30:                  sb.Append("</tr>");
  31:              }
  32:          }
  33:   
  34:          if (cellIndex != 0)
  35:          {
  36:              do
  37:              {
  38:                  sb.Append("<td>&nbsp;</td>");
  39:                  cellIndex++;
  40:              } while (cellIndex < columns);
  41:   
  42:              sb.Append("</tr>");
  43:          }
  44:   
  45:          sb.Append("</table>");
  46:   
  47:          return MvcHtmlString.Create(sb.ToString());
  48:      }
  49:  }

Теперь обо всем по-порядку...
Так как наш метод расширения должен работать с различными типами данных (Person и Product), то в нем использованы generic-тип <T>. Если уж очень просто, то на месте этого самого параметра T может быть любой класс.
DataList<T> - при вызове хелпера, мы должны указать, на какой обьект ему ссылаться;
IEnumerable<T> - список обьектов того же типа, что и в DataList<T>;

Последний параметр расширения, template, имеет тип данных: Func<T, HelperResult>
Такой прием дает нам возможность передать в виде параметра строго типизированное классом T представление.
Сама реализация хэлпера очень проста. Вначале, если коллекция принятых обьектов пуста, мы возвращаем наружу пустую строку. Если же с этим все нормально, тогда начинаем стоить таблицу с помощью StringBuilder-а, и с учетом принятого количества столбцов. Для каждого обьекта из списка мы создаем ячейку, в которую помещаем шаблон частичного представления, заполненный текущим обьектом. В зависимости от принятого кол-ва столбцов мы обрываем строку в нужном месте, таким образом размещая в одной строке ровно столько столбцов, сколько было задано параметром columns.

Хэлпер мы создали, сущности есть (Product, Person), частичные представления для них тоже есть (_productBox, _personBox). Теперь пришло время испытать наше расширение.

Создадим 2 страницы.
На первой будет отображена таблица с товарами:

   1:  @model List<Product>
   2:   
   3:  @using _MyMvcApplication.Helpers;
   4:  @using _MyMvcApplication.Models;
   5:   
   6:  <h2>Products</h2>
   7:   
   8:  @(Html.DataList<Product>(Model, 4,
   9:      @<div class="item-box">
  10:          @Html.Partial("_productBox", item)
  11:       </div>
  12:  ))

На второй - таблица с персонами:

   1:  @model List<Person>
   2:   
   3:  @using _MyMvcApplication.Helpers;
   4:  @using _MyMvcApplication.Models;
   5:   
   6:  <h2>Persons</h2>
   7:   
   8:  @(Html.DataList<Person>(Model, 3,
   9:      @<div class="item-box">
  10:          @Html.Partial("_personBox", item)
  11:       </div>
  12:  ))

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

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

В результате, на странице товаров у нас будет таблица из 4-ех столбцов, с информацией о товаре в каждой ячейке:

А страница персон будет содержать таблицу из 3-ех столбцов, с отображением информации о персонах:

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

_____
Исходники