Рендеринг контента в Orchard CMS

Важной задачей любой CMS является рендеринг контента (Content Rendering или Content Output). Обычно для этого используется какой-нибудь шаблонизатор. Orchard CMS использует Razor.

Shape и другие необходимые понятия

Терминология Orchard CMS может быть спроэцирована на терминологию MVC следующим образом:

  • ContentItem — Model. Объекты которыми оперирует Orchard CMS. Состоит из Part'ов и Field'ов.
  • Driver — Controller. У каждого типа Part'а или Field'а есть соответствующий Driver. Например, для TitlePart есть TitleDriver.
  • Shape — ViewModel. Для одного Part'a или Field'а Driver может сгенерировать один или более Shap'ов. Shap'ы могут содержать другие Shape'ы. Таким образом на выходе получится дерево Shape'ов.
  • В качестве View может выступать обычный Razor-шаблон, а может C#-функция класса IShapeTableProvider помеченая атрибутом [Shape]. Это актуально для такого распространённого Shape, как List. (CoreShapes.cs)

Shape — это сущность которая объединяет в себе ViewModel для рендеринга и параметры рендеринга. Shape имеет имя и альтернативные названия (Metadata.Alternates). Именно по этим именам Orchard пытается найти подходящее View.

Фильтр в теме
Фильтр в теме

В Orchard нет прямого соответствия между контентом и Shape. Например, для TextField Driver создаст только один Shape, а для TitlePart их будет создано целых три. Это сильно усложняет понимание того, как именно будет выглдяеть дерево Shape'ов.

Shape Tracing

Админка Orchard CMS не позволит узнать, из каких Shape'ов будет отрендерена страница и какой именно Razor-шаблон будет использован для каждого Shape. Для ответа на все эти вопросы существует фича Shape Tracing, которую можно включить в админке в меню Modules.

Включение Shape Tracing
Включение Shape Tracing

Shape Tracing по идеологии использования напоминает FireBug: можно навести мышкой на часть HTML-страницы и увидеть все параметры рендеринга.

Просмотр параметров Shape-a

Razor-шаблон связывается с Shape при помощи системы соглашений, основанной на названии Shape и имени файла-шаблона, подробнее об этих соглашениях можно прочесть в официальной документации. На основании этих соглашений вы можете делать связку Shape c шаблоном зависимой от адреса страницы, типа или идентификатора Content Item'a. Shape Tracing позволяет посмотреть все возможные варианты связки шаблона с помощью функции Alternate.

Все возможные имена шаблона
Все возможные имена шаблона

На иллюстрации выше также можно заметить, что Orchard CMS поддерживает механизм тем (Theme), который позволяет подменять базовые шаблоны. Нужную вам тему можно активировать одним кликом в админке.

Shape Tracing позволяет просмотреть код и путь до файла шаблона, использованного при рендеринге Shape.

Просмотр шаблона

Следующим образом выглядит результат рендеринга ветки Shape'ов:

Просмотр результата рендеринга

Благодаря Shape Tracing можно посмотреть данные, которые содержит Shape. Кроме ссылки на исходный ContentItem в Shape часто содержит другие поля, созданные Driver'ом. В приведённом ниже примере TitlePart кроме ContentItem'а содержит ещё поле Title.

Просмотр данных Shape

Ограничения Shape

У Shape Tracing есть существенное ограничение: для каждого Shape View должен генерировать HTML внутри одного тега. На этот тег будет навешан id, и к этим id привяжется Shape Tracer. При нарушении этого требования Shape Tracing будет показывать дерево Shape'ов неправильно: будут отсутствовать ветки.

Например, если View для Shape генерирует два тега без единого родителького, то такой Shape не будет поддерживаться Shape Tracing'ом. Cледующий вывод не поддерживается:

<h1>Title</h1>
<p> 
  text
</p>

Однако Shape Tracing будет поддерживать следующий вывод:

<div>
<h1>Title</h1>
<p> 
  text
</p>
</div>

Вёрстка с лишними div'ами не всегда возможна в реальной жизни, потому я советую использовать механизм тем. Проще сделать одну тему специально для Shape Tracing'a, а вторую для реальной вёрстки.

Кстати, многие стандартные Shape имеют такую вёрстку, что их не показывает Shape Tracer. Потому часто приходится лезть в код и пытаться понять всю иерархию Shape'ов.

Display Type

Shape Tracing показывает значение параметра Display Type для Shape.

Значение Display Type — Summary
Значение Display Type — Summary

Display Type — это строка, которая передаётся в Driver, чтобы тот мог определить в каком контексте его вызвают. В зависимости от этого Driver может генерировать несколько другой набор Shape и с другими данными. Расширяя Orchard можно задавать свои значения Display Type, но по умолчанию в Orchard используются следующие значения параметра Display Type:

  • Summary — если требуется вывести краткие сведения. Например, в случае рендеринга элемента списка.
  • Detail — требуется вывести полную информацию. Например, в случае рендеринга элемента списка на отдельной странице.
  • SummaryAdmin — требуется вывести краткие сведения в админке.

Orchard CMS позволяет сгенерировать дерево shape'ов вручную и отрендерить, прямо из Razor-шаблона следующим образом:

@Display(contentManager.BuildDisplay(contentItem, "Summary"))

Placement.info

Placement.info — это файл с правилами рендеринга Shape'ов. Эти правила позволяют:

  • Запретить рендеринг определённого Shape.
  • Поменять порядок отображения Shape'ов.
  • Добавить Alternate для Shape.

Подробно об этом можно прочитать в статье Understanding placement info. Я только приведу примеры использования Placement.info.

Файлы Placement.info могут находиться в папке модуля и в папке темы. Placement.info из модулей комбинируются с Placement.info текущей темы, при этом у Placement.info темы более высокий приоритет.

Так можно запретить отображение Part'a:

<Placement>
  <Place Parts_Common_Metadata_Summary="-" />
  <Place Parts_Common_Metadata="-" />
</Placement>

Следующий пример показывает, как для всех типов запретить отображать полное описание в режиме Summary и краткое в режиме Detail.

<Placement>
  <Match DisplayType="Summary">
    <Place Parts_FullDescriptionPart="-"/>
  </Match>
  <Match DisplayType="Detail">
    <Place Parts_ShortDescriptionPart="-"/>
  </Match>
</Placement>

Следующий пример описывает правило, по которому при выводе списка стран для каждой страны не будет показан список городов, но если мы выводим полное описание страны, то список городов будет выведен.

<Placement>
  <Match ContentType="Country">
    <Match DisplayType="Summary">
      <Place Parts_Country_CityList="-"/>
    </Match>
    <Match DisplayType="Detail">
      <Place Parts_Country_CityList="Content:11"/>
    </Match>
  </Match>
</Placement>

Стоит заметить, что если мы запрещаем вывод Shape, то его код не выполняется и модель для этого Shape не создаётся. В последнем примере для списка стран из базы не будут выгружаться города.

Далее, используем возможность задать дополнительный Alternate для Shape. В следующем примере сделаем для типа контента MyContentType два разных шаблона.

<Match ContentType="MyContentType">
  <Match DisplayType="Summary">
    <Place Parts_Tags_ShowTags="Content:4;Alternate=Parts_Tags_ShowTag_MyContentType_Summary"/>
  </Match>
  <Match DisplayType="Detail">
    <Place Parts_Tags_ShowTags="Content:4;Alternate=Parts_Tags_ShowTag_MyContentType_Detail"/>
  </Match>
</Match>

Теперь мы можем создать два Razor-шаблона:

/Views/Parts.Tags.ShowTag.MyContentType.Summary.cshtm
/Views/Parts.Tags.ShowTag.MyContentType.Detail.cshtml

Первый применится к Shape Parts_Tags_ShowTags только если он отображается при Display Type равном Summary, а второй если Display Type равен Detail.

Зоны, слои, виджеты

В Orchard CMS страница . Например, Content Item, ассоциированный со страницей, выводится в зону Content. Набор всех возможных зон определяется в теме сайта. По умолчанию в Orchard CMS установлена тема TheThemeMachine, в которой зон более чем достаточно. В вашей собственной теме вы можете определить только нужные вам зоны.

Виджет — это Content Item, который можно поместить в зону. С помощью виджетов удобно выводить различные блоки второстепенного контента вроде списка последних новостей, навигации, и т.д.

Управление виджетами

Для того, чтобы сделать одни виджеты видимыми только на главной странице, вторые на остальных страницах, а третьи на всех вообще страницах, в Orchard CMS есть механизм слоёв (Layers).

Список слоёв
Список слоёв

У каждого слоя есть правила для отображения. Например, вы можете сделать всегда видимый слой и поместить на его виджет с навигацией, другой слой сделать видимым только на главной странице и поместить туда приветственное сообщение. В зависимости от правил отображения слои могут комбинироваться.

Фактически виджет имеет две координаты: зону и слой. Виджет не может находиться одновременно на нескольких слоях сразу.

В других CMS встречается понятие Page Template или Layout. Это нечто, что определяет, какие именно ячейки для контента будут на странице определённого типа. В Orchard CMS такого понятия нет, а есть набор зон, одинаковый для всех страниц. Будет ли рендериться зона или нет, определяется в Razor-шаблоне Layout.cshtml.

Сильно сбивает следующая картинка в админке:

Странная картинка ThemeZonePreview
Странная картинка ThemeZonePreview

Файл с этим изображением называется ThemeZonePreview.png и его наличие предполагается во всех темах. На самом деле, расположение зон определяется шаблоном Layout.cshtml, при этом для разных страниц этот шаблон может быть разным. Таким образом, картинка ThemeZonePreview.png может быть актуальна только для простых сайтов с одним Layout.cshtml, в остальных случаях она вредна, ибо вводит в ступор, особенно при первом знакомстве с CMS.

Как показала практика, плодить зоны про запас, как это сделано в теме TheThemeMachine, не имеет смысла. Зоны можно легко создавать по мере необходимости, просто добавляя их названия в Theme.txt.

Кстати, зоны можно рендерить не только в шаблоне Layout.cshtml, но и в остальных шаблонах. Только доступ к зоне будет через объект Layout.

@Display(Layout.BlogFooter)

Это может быть полезно, если хочется отрендерить виджет внутри вёрстки Content'a или Part'a.

Практика написания Razor-шаблонов для Orchard CMS

Обращение к Field'ам и свойствам Part'a

Если в Razor-шаблоне доступен Content Item, то вы можете обратиться к любому Filed'у любого Part'a этого ContentItem'a.

Model.ContentItem — это переменная типа dynamic, это позволяет обращаться к Field'ам следующим образом:

@Model.ContentItem.PartName.FieldName.Value

В случае, если Field добавлен прямо в Content Type, название Part'a будет такое же, как у типа:

Путь до поля Favorite типа Article

Content Part может иметь свои свойства, за которые отвечает сам Part. Например, свойство Title у TitlePart или Text у BodyPart. В Razor-шаблоне к ним можно обратиться следующим образом: @Model.ContentItem.TitlePart.Title

Подключение скриптов и стилей

В Orchard CMS очень грамотно сделано добавление стилей и скриптов в Razor-шаблоны.

У модуля может быть файл ResourceManifest.cs, в котором определено, какие скрипты и стили содержит этот модуль. Кроме всего прочего, там описаны связи между скриптами. Например, если вы подключаете jQuery UI, то Orchard подключит jQuery перед подключением jQuery UI. В Razor-шаблоне это выглядит следующим образом:

@{    
    Script.Require("jQueryUI").AtHead();
}

Метод AtHead() и AtFoot() определяют куда будет вставлен тег <script>: внутрь тега head или в конец body.

Скрипты и стили темы можно подключать следующим образом:

@{
    Style.Include("master.css");    
    Script.Include("common.js").AtHead();
}

Если Shape'у нужно вставить немного JavaScript'а в конец HTML-страницы, то сделать это можно следующим образом:

@using(Script.Foot())
{
	<script type="text/javascript">
	    $().ready(function () {
	        alert('Hello world');
	    });            
	</script>
}

В своей теме можно создать ResourceManifest.cs и описать там CSS и JS ресурсы и связи между ними. Например, следующим образом:

public class ResourceManifest : IResourceManifestProvider
    {
        public void BuildManifests(ResourceManifestBuilder builder)
        {
            var manifest = builder.Add();
            manifest.DefineScript("ES_JQuery")
                .SetUrl("jquery-1.8.3.min.js");
            manifest.DefineScript("ES_Boostrap")
                .SetUrl("bootstrap.min.js");                           
            manifest.DefineScript("ES_Main")
                .SetUrl("jquery.main.js")
                .SetDependencies(new[] {"ES_JQuery", "ES_Boostrap"});
        }
    }

Затем, можно подключить ES_Main в CSHTML следующим образом: Script.Require("ES_Main").AtHead() и Orchard сгенерирует подключение всех зависимых скриптов.

Путь до картинки

Картинки, как правило, включены в тему. Если вы хотите получить путь до картинки в вашей теме, то вам потребуется следующий код:

<img src='@Url.Content(Html.ThemePath(WorkContext.CurrentTheme, "/Content/Header.jpg"))' alt='' />

Ссылка на Content Item

Генерировать ссылку на Content Item в Razor-шаблоне можно с помощью функции Url.ItemDisplayUrl следующим образом:

@using Orchard.ContentManagement
<div>
	<a href="@Url.ItemDisplayUrl((IContent)Model.ContentItem)">@Model.ContentItem.TitlePart.Title</a>
</div>

Кеширование

В Orchard CMS есть кеширование на уровне доступа к базе, есть кеширование на уровне бизнес объектов. А есть модуль Output Cache, который позволяет закешировать результат рендеринга HTML всей страницы. Раньше, это был отдельный модуль Contrib.Cache. Частичного кеширования HTML в Orchard нет, и это очень грустно, особенно если вспомнить Umbraco 4.X. В этом видео Bertrand Le Roy показывает, как Cache сломал ему Random-сортировку для Projection Widget'a.

Заключение

Рендеринг контента в Orchard CMS имеет своеобразную идеологию основанную на дереве Shape'ов. Управлять отображением контента из админки нельзя, потому Shape Tracing является основным инструментом при разработке шаблонов.