Как и обещал продолжение поста про отображение иерархических данных с использованием 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, т.к. в данный момент активно занимаюсь разработкой именно на этой платформе.