Работа с иерархическими данными в ASP.Net MVC. Часть 2

В предыдущей статье я описал модель для иерархии категорий, а также показал как на основе модели создать схему базы данных для сохранения ее состояния. В этой части я опишу создание контроллера и 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 самой себя для рендеринга дочерних категорий. Таким образом мы переиспользовали код и минимизировали его количество.

В итоге у нас получилось вот такое дерево с категориями:

image_thumb

Это все, что я хотел рассказать об отображении иерархических данных в ASP.Net MVC. В следующей части я опишу как создать CRUD операции для таких данных.

Реклама

Об авторе sadomovalex

Старший инженер, team lead, консультант. Работаю в стеке .Net. Последние несколько лет занимаюсь разработкой enterprise приложений под Sharepoint, чему и будет в основном посвящена тематика этого блога. Также активно использую и интересуюсь ASP.Net MVC, DDD, TDD, Agile. Активно участвую в жизни многих профессиональных сообществ, SPb .Net UG, SPb ALT.Net, rsdn, Finland SP UG и др.
Запись опубликована в рубрике ASP.Net MVC, DDD, IoC. Добавьте в закладки постоянную ссылку.

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s