среда, 19 января 2011 г.

Отображение иерархических данных с использованием asp.net mvc, часть 2

Как и обещал продолжение поста про отображение иерархических данных с использованием asp.net mvc

В прошлый раз я остановился на том, как выглядит представление:

<%
 var product = (Product) ViewData["Product"];
 var childs = (IEnumerable<Product>)ViewData["Childs"];
%>

<p><%: product.Name %></p>
<ul>
<% foreach(var child in childs) { %>
<li><%: child.Name %></li>
<% } %>
</ul>

* This source code was highlighted with Source Code Highlighter.

Для того, чтобы это представление можно было реально использовать - просматривать с его помощью иерархическую структуру, нужно генерировать ссылки на дочерние объекты. Для этого нужно иметь возможность получать всю цепочку иерархии объектов, вплоть до корня. В интерфейсе IProductService (и его реализации) есть функция IProductService.GetAllParents(Product) которая рекурсивно получает родителя текущего объекта, затем пытается получить родителя - родителя .. и т.д. (об устройстве IProductService'а и вообще о архитектуре я расскажу в другой раз).

Немного расширим действие Index контроллера Catalog добавив получение всей цепочки иерархии объекта (также надо не забывать проверять это поведение в тестах):

var allParents = _productService.GetAllParents(product);

// ...

ViewData["AllParents"] = allParents;


* This source code was highlighted with Source Code Highlighter.

Таким образом у нас в представлении есть все нужные объекты, чтобы сгенерировать ссылки на дочерние объекты, посмотрим как это делается:

  <%
    var product = (Product) ViewData["Product"];
    var childs = (IEnumerable<Product>)ViewData["Childs"];
    var allParents = (((IEnumerable<Product>) ViewData["AllParents"])).Reverse();
  %>

  <h1><%: product.Name %></h1>
  <ul>
    <% foreach(var child in childs) { %>
      <li><%= Html.ActionLink(child.Name, "Index", new
{ path = Html.GenerateHierarchyLink(allParents.Concat(new[] { product }).Concat(new[] { child })) })%></li>
    <% } %>
  </ul>


* This source code was highlighted with Source Code Highlighter.

Как видно из кода, у нас используется расширение для HtmlHelper'а , оно очень простое:

  public static class GenerateHierarchyLinkHelper
  {
    public static string GenerateHierarchyLink(
this HtmlHelper htmlHelper,
IEnumerable<Product> products)
    {
      return string.Join("/", products.Skip(1).Select(x => x.Name));
    }
  }


* This source code was highlighted with Source Code Highlighter.

Здесь с помощью Linq выбираются имена всех объектов за исключением первого - первый - корневой объект это catalog, наш контроллер называется также - Catalog, поэтому ссылка на элемент каталога выглядела бы не очень хорошо:
/catalog/catalog/some/product

Что же покажет представление пользователю, который зашел по ссылке?
/catalog/some/product

при этом иерархия объектов в бд такая:
catalog
|
some
|
product
|
--- child_1
|
--- child_2
|
--- child_3

сначала пользователь увидит заголовок h1 с именем текущего продута, затем маркированный список со ссылками на дочерние продукты
- [a="/catalog/some/product/child_1"]child_1[/a]
- [a="/catalog/some/product/child_3"]child_2[/a]
- [a="/catalog/some/product/child_2"]child_3[/a]

перейдя по которым можно увидеть соответствующие вложенные объекты в иерархии, при этом строка адреса в браузере будет корректно отображать seo-url (а не чтото вроде /catalog/1/2/3)

итак , с первой проблемой - отображение ссылок на дочерние объекты иерархии - разобрались, теперь проблема номер 2 - кастомизация отображения текущей страницы (/Catalog/Index) для некоторых продуктов

Возьмем предыщую иерархию:

catalog
|
some
|
product
|
--- child_1
|
--- child_2
|
--- child_3

Что хочется ? чтобы для продуктов some и product отображалась своя версия представления Index , а для всех остальных - обычная версия, без какого либо дополнительного контента.., также хочется, чтобы этот дополнительный контент дизайнер/владелец/администратор сайта (вообщем человек не знакомый с программированием) мог менять при желании. Варианты решения:
1) В коде представления писать услования вроде:

<% if(product.Name == "product123") %>
<%= "Дополнительный конент" %>
<% if(product.Name == "product456") %>
<%= "Другой дополнительный конент" %>
<% else %>
<%= "Представление по умолчанию" %>


* This source code was highlighted with Source Code Highlighter.

Имхо, не очень хорошее решение, т.к. файл представления превратится в одну большую страницу, в которой трудно будет чтото найти и исправить

2) Сделать так, чтобы для тех продуктов представления которых нужно кастомизировать отображались свои, отдельные версии (которые будут распологаться в отдельных файлах). Т.е. имея вышеописанную иерархию и требования заказчика по своему отображать продукты расположенные тут:

catalog
|
some
|
product

для продукта "catalog->some" физически отображать файл:
/Views/Catalog/some.aspx
а для продукта "catalog->some->product" физически отображать файл:
/Views/Catalog/some/product.aspx
ну и т.д.
(при необходимости можно добавлять такие представления даже в райнтайме)

Как это сделать ?

В конце действия Index контроллера Catalog возвращается объект типа View()

public ActionResult Index(string path)
{
// ..
 return View();
}


* This source code was highlighted with Source Code Highlighter.

Объект View() это экземпляр текущего движка представления (по умолчанию ASP.NET WebForms), asp.net mvc ищет файл с именем Index.aspx (или .ascx) в папках:
/Views/Catalog/
или
/Views/Shared
нам же нужно, чтобы framework искал этот файл внутри папки /Views/Catalog (в том числе и во вложенных в него папках). Для того, чтобы указать, где искать, можно например создать собственный ViewResult (наследовав его от базового ViewResult) и переопределить метод:
ViewEngineResult FindView(ControllerContext context)

* This source code was highlighted with Source Code Highlighter.

чем мы щас и займемся:

  public class CatalogViewResult : ViewResult
  {
    public CatalogViewResult(ViewDataDictionary viewData)
    {
      ViewData = viewData;
    }

    protected override ViewEngineResult FindView(ControllerContext context)
    {
      var product = (Product)ViewData["Product"];
      var allParents = (((IEnumerable<Product>)ViewData["AllParents"])).Reverse();

      var
pathToView = GenerateHierarchyLinkHelper.GenerateHierarchyLink(null,
allParents.Concat(new[] { product }));
      if (!string.IsNullOrWhiteSpace(pathToView))
      {
        var result = ViewEngines.Engines.FindView(context, pathToView, null);
        if (result.View != null)
          return result;
      }

      return base.FindView(context);
    }
  }


* This source code was highlighted with Source Code Highlighter.

Как видно, код достаточно простой, в переопределенной функции FindView() с помощью, уже знакомого нам расширения для HtmlHelper'а определяется иерархический путь:

var pathToView = GenerateHierarchyLinkHelper.GenerateHierarchyLink(null,
allParents.Concat(new[] { product }));


* This source code was highlighted with Source Code Highlighter.

а затем, с помощью стандартного движка представлений ищется представление поэтому пути:
        var result = ViewEngines.Engines.FindView(context, pathToView, null);
        if (result.View != null)
          return result;


* This source code was highlighted with Source Code Highlighter.

В случае его нахождения - т.е. в случае когда файл с кастомизированным представлением существует - оно возвращается в контроллер и тот использует его, чтобы отобразить данные, в случае, если кастомное представление не найдено - возвращается стандартный вариант /Views/Catalog/Index.aspx

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

Ура, я написал свой первый пост, который, как мне кажется будет полезен кому-нибудь. Следующие сообщения также планирую посвятить ASP.NET MVC, т.к. в данный момент активно занимаюсь разработкой именно на этой платформе.

Комментариев нет:

Отправить комментарий