В предыдущей статье я описал модель для иерархии категорий, а также показал как на основе модели создать схему базы данных для сохранения ее состояния. В этой части я опишу создание контроллера и view для отображения дерева категорий в UI.
Мы хотим, чтобы наш список категорий отображался слева для каждой страницы сайта (как например на http://amazon.com). Для этого нам нужен CategoryController и Action для отображения категорий в иерархическом виде:
public class CategoryController : ConventionController
{
private ICategoryRepository categoryRepository;
private IMapper<Category, CategoryListModel> mapperListModel;
// ...
public CategoryController(ICategoryRepository categoryRepository, IMapper<Category, CategoryListModel> mapperToListModel)
{
this.categoryRepository = categoryRepository;
this.mapperListModel = mapperToListModel;
}
public ActionResult List(int? Category)
{
var activeCategories = this.categoryRepository.GetActiveOrphansOrderBy(c => c.Name);
var viewCategories = this.mapperListModel.Map(activeCategories);
if (Category != null)
{
this.setExpanded(Category.Value, viewCategories);
}
return View(viewCategories);
}
}
В качестве IoC контейнера я использовал StructureMap, но для этой статьи это не важно – вы можете использовать любой IoC контейнер для управления зависимостями. Метод setExpanded() просто пробегает по всем категориям рекурсивно и выставляет свойство IsExpanded = true для выбранной категории. Имея CategoryController и Action для него, мы можем написать в master pag-е:
<div class="categoriesMenu">
<% Html.RenderAction<CategoryController>(c => c.List((int?)this.ViewData["CategoryId"]));
%>
</div>
Здесь я использовал типизированный вариант RenderAction из ASP.Net MVC Futures. CategoryId из ViewData содержит идентификатор выбранной в данный момент категории (он понадобится для того, чтобы раскрывать дерево категорий в соответствии с выбором пользователя). Вернемся к действию (action) List нашего контроллера. Сначала мы выбираем категории высшего уровня (метод GetActiveOrphansOrderBy репозитория). Здесь можно поспорить относится сортировка (в данном случае по имени категорий x => x.Name) к репозиторию или нет. Я вынес ее в репозиторий, потому что так мне было удобнее. Далее нам нужно из модели предметной области получить модель представления:
public class CategoryListModel
{
public int Id { get; set; }
public string Name { get; set; }
public CategoryListModel ParentCategory { get; set; }
public List<CategoryListModel> ChildCategories { get; set; }
public int NestingLevel
{
get
{
if (this.ParentCategory == null)
{
return 0;
}
return (this.ParentCategory.NestingLevel + 1);
}
}
public bool IsExpanded { get; set; }
public bool IsActive { get; set; }
public bool IsSelected { get; set; }
public bool HasChildCategories
{
get { return (!this.ChildCategories.IsNullOrEmpty()); }
}
}
Я думаю, вы уже догадались зачем нам нужна модель представления (view model). Как видно из кода, она содержит свойства нужные исключительно для конкретного случая отображения нашей модели в виде иерархий. При использовании любого другого вида отображения мы просто сделаем еще один вариант модели отображения. В обоих случаях наша модель предметной области остается “чистой” от деталей, не имеющих прямого отношения к бизнесу.
Для отображения моделей, как я уже говорил, используем AutoMapper (this.mapperListModel.Map(activeCategories)). Вот простая обертка над ним:
public class Mapper<T, U> : IMapper<T, U>
{
public U Map(T source)
{
return Mapper.Map<T, U>(source);
}
public IEnumerable<U> Map(IEnumerable<T> source)
{
return Mapper.Map<IEnumerable<T>, IEnumerable<U>>(source);
}
public T Map(U source)
{
return Mapper.Map<U, T>(source);
}
public IEnumerable<T> Map(IEnumerable<U> source)
{
return Mapper.Map<IEnumerable<U>, IEnumerable<T>>(source);
}
}
При использовании AutoMapper-а нам нужно описать конфигурацию отображения (так же, как мы делали для NHibernate – см. выше):
Mapper.CreateMap<Category, CategoryListModel>()
.ForMember(x => x.NestingLevel, o => o.Ignore())
.ForMember(x => x.IsExpanded, o => o.Ignore())
.ForMember(x => x.IsSelected, o => o.Ignore())
.ForMember(x => x.HasChildCategories, o => o.Ignore())
.AfterMap((category, categoryModel) =>
{
// Mapper doesn't know about categories relationship, so we need assign them explicitly.
// Without it setting IsExpanded = true will not have effect on childCategory.ParentCategory
// as this is another object
if (categoryModel == null || categoryModel.ChildCategories.IsNullOrEmpty())
{
return;
}
foreach (var childCategory in categoryModel.ChildCategories)
{
childCategory.ParentCategory = categoryModel;
}
});
Я специально не стал удалять свой комментарий к коду, который позволяет правильно связать ParentCategory у всех дочерних категорий с их родителем.
Нам осталось только создать view для отображения дерева категорий. Это должен быть partial view, т.к. мы хотим использовать его в master page интернет-магазина. В качестве ViewEngine я использовал WebForms, поэтому код не такой выразительный как для Spark или Razor. Тем не менее, идея будет понятна:
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<System.Collections.Generic.IEnumerable<CategoryListModel>" %>
<%@ Import Namespace="UI.Controllers" %>
<%@ Import Namespace="UI.Helpers" %>
<% foreach (var categoryListModel in this.Model)
{%>
<% if (categoryListModel.ParentCategory == null || categoryListModel.ParentCategory.IsExpanded)
{ %>
<span>
<% for (int i = 0; i < categoryListModel.NestingLevel; i++)
{%>
<%} %>
<%= Html.ActionLink<ProductController>(c => c.List(categoryListModel.Id, 1), categoryListModel.Name,
categoryListModel.IsSelected ? new { @class = "categoriesSelected categoriesList" } :
new { @class = "categoriesList" })%>
<%
if (categoryListModel.HasChildCategories)
{
if (categoryListModel.IsExpanded)
{
%>
<img alt="" src="<%= Url.Content("~/Content/images/arrow-down-light.png") %>" />
<%
}
else
{
%>
<img alt="" src="<%= Url.Content("~/Content/images/arrow-right-light.png") %>" />
<%
}
}
%>
</span>
<br />
<% } %>
<% if (categoryListModel.IsExpanded)
{ %>
<% Html.RenderPartial("List", categoryListModel.ChildCategories); %>
<% } %>
<%} %>
Для отображения категорий в виде дерева мы проходим в цикле по всем категориям высшего уровня иерархии (у которых нет родительской категории). Для каждой категории мы формируем ссылку используя ProductController – для того чтобы получить ссылку на список товаров данной категории в виде http://example.com/Product/List/Category/1. При этом мы используем такие свойство модели представления, как NestingLevel (для того, чтобы задать отступ – чем больше уровень вложенности, тем больше отступ), IsSelected (для установки css стиля выбранного элемента, который, например, устанавливает другой цвет), HasChildCategories и IsExpanded (для отображении стрелки рядом с категорией, которая имеет подкатегории). Обратите внимание на важный момент: свойство IsExpanded используется для рекурсивного вызова view самой себя для рендеринга дочерних категорий. Таким образом мы переиспользовали код и минимизировали его количество.
В итоге у нас получилось вот такое дерево с категориями:
Это все, что я хотел рассказать об отображении иерархических данных в ASP.Net MVC. В следующей части я опишу как создать CRUD операции для таких данных.

