Orchard CMS

Расширяемость Orchard

Бертран Ле Руа

Продукты и технологии:

Orchard CMS

В статье рассматриваются:

  • типы и элементы контента
  • создание фрагмента контента (content part)
  • записи базы данных
  • класс Part
  • драйверы фрагмента контента (content part)
  • рендеринг форм (shapes)
  • расширения упаковки
  • интерфейсы расширения

Исходный код можно скачать по ссылке.

У большинства веб-приложений много общего и в то же время немало различий. Во всех них есть статические страницы («Terms of Use», «About Us» и т. д.). Они представляют контент со стандартной структурой. У них есть навигационные меню. Они могут поддерживать поиск, комментарии, рейтинги и интеграцию с социальными сетями. Но некоторые из них являются блогами, другие продают книги, третьи обеспечивают связь между друзьями, а какие-то содержат сотни тысяч справочный статей по вашим любимым технологиям.

Content Management System (CMS) (система управления контентом) предназначена для того, чтобы предоставлять общие фрагменты, не накладывая никаких ограничений на тип создаваемого сайта. Это весьма искусное упражнение в расширяемости.

Создатели Orchard CMS (orchardproject.net) — я в их числе — выбрали подход, который фундаментально опирается на композицию и соглашения. В этой статье я представлю несколько примеров простых расширений системы, которые должны стать хорошей отправной точкой для ваших модулей.

Система динамических типов

Независимо от того, какую CMS вы используете при построении своего сайта, у вас будет некая центральная сущность контента, которая в разных системах называется по-разному. В Drupal она называется узлом (node), а в Orchard — элементом контента (content item). Эти элементы являются атомами контента, например публикациями в блоге, страницами, товарами, виджетами или обновлениями состояния. Некоторые из них соответствуют какому-то URL, другие — нет. Их схемы сильно различаются, но общее между ними заключается в том, что это самые малые единицы контента на сайте. Или нет?

Расщепление атома

Наша первая реакция как разработчиков — идентифицировать элементы контента в качестве экземпляров классов (публикация, страница, товар или виджет), что в какой-то мере правильно. По аналогии с классами, которые состоят из членов (полей, свойств и методов) типы контента («классы» элементов контента) являются составными объектами. Но они состоят не из простых свойств с типами, которые представляют собой классы, а из фрагментов контента — атомов его поведения. Это важное различие, которое я проиллюстрирую на примере.

Публикация в блоге обычно состоит из URL, заголовка, даты, тела сообщения с форматированием, сиска тегов и списка комментариев пользователей. Ни один из этих фрагментов не является специфичным для публикации в блоге. Специфична лишь композиция этих фрагментов, а не сами фрагменты.

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

Независимо от того, какую CMS вы используете при построении своего сайта, у вас будет некая центральная сущность контента, которая в разных системах называется по-разному.

Другой факт о CMS-системах заключается в том, что тип контента не фиксируется заранее. Публикации в блоге раньше были простым текстом, но очень быстро стали превращаться в нечто совсем другое. Теперь они, как правило, содержат видеоролики, подкасты или галереи изображений. Если ваш блог посвящен путешествиям по миру, то, возможно, вы захотите добавить в блог поддержку геопозиционирования.

И вновь на помощь приходят фрагменты контента. Вам нужна долгота и широта? Расширьте тип публикации в блоге, добавив картографический фрагмент (у нас есть несколько таких фрагментов в нашей галерее модулей gallery.orchardproject.net). Когда задумываешься об этом, быстро становится очевидным, что эта операция добавления фрагмента к существующему типу будет выполняться чаще всего владельцем сайта, а не разработчиком. Следовательно, она должна быть возможной не только через добавление свойства к типу Microsoft .NET Framework. Операция должна управляться метаданными и происходить в период выполнения, чтобы можно было создать администраторский UI для выполнения этой задачи (рис. 1).

The Orchard Content Type Editor
Рис. 1. Orchard Content Type Editor

Это первый способ расширить Orchard. При необходимости вы в любой момент можете создавать и расширять типы контента через администраторский UI. Конечно, все, что можно делать через этот UI, можно делать и программным способом, например:

item.Weld(part);

Этот код динамически «приваривает» (welds) какой-то фрагмент к элементу контента. Это интересная возможность, так как позволяет динамически расширять экземпляры типов в период выполнения. В динамических языках такое называют подмешиванием (mix-in), но об этой концепции почти ничего не известно в статически типизируемых языках вроде C#. Она открывает новые возможности, но это не совсем то, что мы делали из администраторского UI. Кроме того, мы хотим иметь возможность добавлять фрагмент именно к типу, а не к каждому из его экземпляров:

ContentDefinitionManager.AlterTypeDefinition(
  "BlogPost", ctb => ctb.WithPart("MapPart")
  );

Именно так определяется тип контента BlogPost:

ContentDefinitionManager.AlterTypeDefinition("BlogPost",
  cfg => cfg
    .WithPart("BlogPostPart")
    .WithPart("CommonPart", p => p
      .WithSetting("CommonTypePartSettings.
        ShowCreatedUtcEditor", "true"))
      .WithPart("PublishLaterPart")
      .WithPart("RoutePart")
      .WithPart("BodyPart")
  );

Вероятно, вы заметили в этом фрагменте кода, что теги и комментарии исчезли из публикаций в блоге. Это еще один пример тщательного разделения обязанностей. Модуль блога на самом деле ничего не знает о тегах и комментариях, равно как модули тегов и комментариев ничего не знают о блогах. За их соединение отвечает третья сила.

Вкусные рецепты

При установке выполняется рецепт (recipe), отвечающий за этот тип задач. Это XML-описание начальной конфигурации сайта. Orchard поставляется с тремя рецептами: blog, default и core. В следующем коде показан фрагмент рецепта blog, которая добавляет к публикациям в блоге теги и комментарии:

<BlogPost ContentTypeSettings.Draftable="True"
  TypeIndexing.Included="true">
  <CommentsPart />
  <TagsPart />
  <LocalizationPart />
</BlogPost>

До сих пор я показывал различные способы, с помощью которых фрагменты контента можно составлять в элементы контента. Далее я поясню, как создавать собственные фрагменты.

Создание фрагмента

Чтобы проиллюстрировать создание нового фрагмента, я намерен опереться на пример функции Meta из своего модуля Vandelay Industries (его можно скачать по ссылке bit.ly/u92283). Функция Meta добавляет свойства для ключевых слов и описания с целью оптимизации под поисковые системы (Search Engine Optimization, SEO) (рис. 2).

The SEO Meta Data Editor
Рис. 2. SEO Meta Data Editor

Эти свойства преобразуются в разделе head страницы в стандартные метатеги, понятные поисковым системам:

<meta content="Orchard is an open source Web CMS
  built on ASP.NET MVC."  name="description" />
<meta content="Orchard, CMS, Open source" name="keywords" />

Запись

Первая часть головоломки — описание способа, которым данные будут сохраняться в базе данных. Строго говоря, не для всех фрагментов нужны записи, поскольку не все они хранят свои данные в базе данных Orchard, но большинство делает это. Запись является обычным объектом:

public class MetaRecord : ContentPartRecord {
  public virtual string Keywords { get; set; }
  public virtual string Description { get; set; }
}

Класс MetaRecord наследует от ContentPartRecord. В этом нет абсолютной необходимости, но определенно удобно, так как избавляет от возни с некоторой частью инфраструктуры. В этом классе два строковых свойства: для ключевых слов и описания. Эти свойства должны быть виртуальными, чтобы инфраструктура могла «подмешивать» собственную логику и создавать конкретный класс, используемый в период выполнения.

Единственная обязанность записи — сохранение в базе данных посредством объявления механизма хранилища, который можно найти в MetaHandler:

public class MetaHandler : ContentHandler {
  public MetaHandler(
    IRepository<MetaRecord> repository) {
      Filters.Add(StorageFilter.For(repository));
  }
}

Кроме того, хранилище нужно инициализировать. В ранних версиях Orchard схема базы данных логически определялась на основе классов записей, но на одних логических выводах далеко не уедешь, поэтому такой механизм был заменен более точной системой миграции (migration system), где модификации схемы определяются явным образом, как показано на рис. 3.

Рис. 3. Явно определяемые модификации схемы

public class MetaMigrations : DataMigrationImpl {
  public int Create() {
    SchemaBuilder.CreateTable("MetaRecord",
      table => table
        .ContentPartRecord()
        .Column("Keywords", DbType.String)
        .Column("Description", DbType.String)
    );

    ContentDefinitionManager.AlterPartDefinition(
      "MetaPart", cfg => cfg.Attachable());
    return 1;
  }
}

Таблица MetaRecord создается с именем, которое система сможет по соглашению сопоставить с классом MetaRecord. В ней есть системные столбцы для записи фрагмента контента, добавляемой вызовом метода ContentPartRecord, плюс строковые столбцы Keywords и Description, автоматически сопоставляемые по соглашению с соответствующими свойствами класса записи.

Вторая часть метода миграции сообщает, что новый фрагмент будет подключаемым к администраторскому UI для любого существующего типа контента.

Метод Create всегда представляет начальную миграцию и обычно возвращает 1 — номер миграции (migration number). По соглашению, в будущих версиях модуля разработчик может добавить методы UpdateFromX, где X заменяется номером миграции, в которой работает этот метод. Этот метод должен возвращать новый номер миграции, который соответствует новому номеру миграции схемы. Эта система обеспечивает независимое и гибкое обновление всех компонентов.

Для представления реального фрагмента используется отдельный класс, который мы сейчас и рассмотрим.

Класс Part

Представление самого фрагмента является классом, производным от ContentPart<TRecord>:

public class MetaPart : ContentPart<MetaRecord> {
  public string Keywords {
    get { return Record.Keywords; }
    set { Record.Keywords = value; }
  }

  public string Description {
    get { return Record.Description; }
    set { Record.Description = value; }
  }
}

Фрагмент действует как прокси для свойство Keywords и Description записи (для удобства), но, если бы этого не делалось, запись и ее свойства все равно были бы доступны через открытое свойство Record базового класса ContentPart.

В любом коде, где есть ссылка на какой-либо элемент контента, в котором имеется фрагмент MetaPart, сможет задействовать преимущества строго типизированного доступа к свойствам Keywords и Description. Для этого он должен вызывать метод As, который является аналогом CLR-операции приведения типов в системе типов Orchard:

var metaKeywords = item.As<MetaPart>().Keywords;

Класс фрагмента также является местом, где вы реализуете любое специфическое поведение данных этого фрагмента. Например, составной продукт мог бы предоставлять методы или свойства для доступа к своим подпродуктам или для вычисления общей стоимости.

Поведение, которое относится к взаимодействию с пользователем (координирующий код в контроллере любого неспецифического приложения ASP.NET MVC), — дело другое. Здесь в игру вступают драйверы.

Драйвер

Каждый фрагмент в элементе контента должен иметь возможность участвовать в жизненном цикле запроса и в конечном счете выполнять работу контроллера ASP.NET MVC, но делать это нужно на уровне фрагмента, а не самого запроса. Драйвер фрагмента контента выступает в роли уменьшенного контроллера (scaled-down controller). Он не имеет полной функциональности контроллера в том плане, что отсутствует сопоставление его методов с маршрутами (routes). Вместо этого он состоит из методов, обрабатывающих определенные события, такие как Display или Editor. Драйвер просто является классом, производным от ContentPartDriver.

Драйвер фрагмента контента выступает в роли уменьшенного контроллера.

Именно метод Display вызывается, когда Orchard нужно выполнить рендеринг фрагмента в форме только для чтения (рис. 4).

Рис. 4. Метод Display драйвера подготавливает рендеринг фрагмента

protected override DriverResult Display(
  MetaPart part, string displayType, dynamic shapeHelper) {
    var resourceManager =
      _wca.GetContext().Resolve<IResourceManager>();
    if (!String.IsNullOrWhiteSpace(part.Description)) {
      resourceManager.SetMeta(new MetaEntry {
        Name = "description",
        Content = part.Description
      });
    }
    if (!String.IsNullOrWhiteSpace(part.Keywords)) {
      resourceManager.SetMeta(new MetaEntry {
        Name = "keywords",
        Content = part.Keywords
      });
    }
  return null;
}

Этот драйвер на самом деле несколько нетипичен, поскольку большинство драйверов просто осуществляют рендеринг по месту (подробнее об этом чуть ниже), тогда как фрагмента Meta должен выполнять рендеринг своих метатегов в разделе head. Этот раздел в HTML-документе является общим ресурсом, поэтому нужны особые предосторожности. Orchard предоставляет API-средства для доступа к этим общим ресурсам, которые вы и видели здесь: я задаю метатеги через диспетчер ресурсов. Этот диспетчер позаботится о рендеринге реальных тегов.

Метод возвращает null, так как в данном сценарии нет ничего для рендеринга по месту. Методы большинства драйверов будут возвращать вместо этого динамический объект, который называется формой (shape), что аналогично модели представления в ASP.NET MVC. Я вернусь к формам чуть позже, когда дело дойдет до их рендеринга в HTML, а пока достаточно сказать, что это очень гибкие объекты. В них можно помещать все релевантное для шаблона, который будет выполнять соответствующий рендеринг, без необходимости создания специального класса:

protected override DriverResult Editor(MetaPart part,
  dynamic shapeHelper) {
  return ContentShape("Parts_Meta_Edit",
    () => shapeHelper.EditorTemplate(
      TemplateName: "Parts/Meta",
      Model: part,
      Prefix: Prefix));
}

Метод Editor отвечает за подготовку и визуализацию UI редактора для фрагмента. Обычно он возвращает специальный вид формы (shape), подходящей для создания UI, обеспечивающих редактирование композиции (composite edition).

Кроме того, в драйвере есть метод, который обрабатывает данные, передаваемые редактором:

protected override DriverResult Editor(MetaPart part,
  IUpdateModel updater, dynamic shapeHelper) {
  updater.TryUpdateModel(part, Prefix, null, null);
  return Editor(part, shapeHelper);
}

Настройки сайта, по сути, определяются элементами контента в Orchard, что становится понятным, как только вы вникнете в принципы управления мультитенантностью в Orchard.

Код в этом методе вызывает TryUpdateModel для автоматического обновления фрагмента данными, возвращенными редактором. Выполнив эту задачу, он вызывает первый метод Editor, чтобы вернуть ту же форму редактора (editor shape), которая была создана.

Рендеринг форм

Обращаясь ко всем драйверам для всех фрагментов, Orchard создает дерево форм — модель большого составного и динамического представления для всего запроса. Следующая задача системы — определить, как разрешить все формы в шаблоны, которые смогут осуществлять их рендеринг. Для этого она анализирует имя каждой формы (Parts_Meta_Edit в случае метода Editor) и пытается сопоставить его с файлами в известных местах системы, например в папках Views текущей темы и модуля. Это важная точка расширения, так как она позволяет переопределять рендеринг по умолчанию любого контента в системе простым размещением файла с правильным именем в папке вашей локальной темы.

В папку Views\EditorTemplates\Parts моего модуля я поместил файл с именем Meta.cshtml (рис. 5).

Рис. 5. Шаблон редактора для MetaPart

@using Vandelay.Industries.Models
@model MetaPart

<fieldset>
  <legend>SEO Meta Data</legend>

  <div class="editor-label">
    @Html.LabelFor(model => model.Keywords)
  </div>
  <div class="editor-field">
    @Html.TextBoxFor(model => model.Keywords,
      new { @class = "large text" })
    @Html.ValidationMessageFor(model => model.Keywords)
  </div>

  <div class="editor-label">
    @Html.LabelFor(model => model.Description)
  </div>
  <div class="editor-field">
    @Html.TextAreaFor(model => model.Description)
    @Html.ValidationMessageFor(model => model.Description)
  </div>

</fieldset>

Контентом является все

Прежде чем перейти к другим возможностям расширения, я хотел бы упомянуть вот что. Стоит разобраться в системе типов элементов контента, и вы поймете самую важную концепцию в Orchard. Многие важные сущности системы определены как элементы контента. Например, пользователь является элементом контента, что позволяет модулям профиля добавлять к нему произвольные свойства. У нас также есть элементы контента «виджет», которые можно визуализировать в зонах, определенных в теме. Именно так в Orchard создаются формы поиска, архивы блога, облака тегов и другие UI в боковой колонке (sidebar UI). Но самым удивительным применением элементов контента может оказаться сам сайт. Настройки сайта, по сути, определяются элементами контента в Orchard, что становится понятным, как только вы вникнете в принципы управления мультитенантностью в Orchard. Если вы хотите добавить собственные настройки сайта, вам достаточно добавить фрагмент к типу контента Site, и вы создадите администраторский UI для его редактирования, после чего сможете выполнить все те же операции, о которых я вкратце рассказал ранее. Унифицированная система расширяемых типов контента — концепция очень функциональная.

Тема — это, как правило, набор изображений, таблиц стилей и переопределений шаблонов, упакованных в подкаталог в каталоге Themes.

Упаковка расширений

Я показал, как создавать собственные фрагменты для Orchard, и упоминал концепции модулей и тем без пояснения этих терминов. Если в двух словах, то они являются единицами развертывания в системе. Расширения распространяются как модули, а визуальные компоненты — как темы.

Тема — это обычно набор изображений, таблиц стилей и переопределений шаблонов, упакованных в подкаталог в каталоге Themes. Кроме того, в ней имеется файл манифеста theme.txt, используемый в качестве корня для определения метаданных, например сведений об авторе темы.

Аналогично модуль — это подкаталог в каталоге Modules. Это тоже область ASP.NET MVC с несколькими изменениями. Например, модулю нужен дополнительный файл манифеста module.txt, в котором объявляются некоторые метаданные для этого модуля, такие как автор, веб-сайт, название функций, зависимости или номер версии.

Будучи только одной областью более крупного сайта, модуль должен корректно работать с несколькими общими ресурсами. Например, используемые им маршруты должны быть определены классом, реализующим IRouteProvider. Orchard создаст полную таблицу маршрутов на основе того, что предоставляется всеми модулями. Аналогично модули могут участвовать в создании администраторского меню, реализуя интерфейс INavigationProvider.

Стоит отметить, что код для модулей обычно не предоставляется в виде скомпилированных двоичных файлов (хотя технически это возможно). Это наше осознанное решение, чтобы дать вам возможность модифицировать модули: вы скачиваете какой-то модуль из галереи и изменяете его под свои конкретные потребности. Возможность модификации кода для чего угодно — одна из сильных сторон таких PHP CMS, как Drupal или WordPress, и мы хотели обеспечить ту же гибкость в Orchard. При скачивании модуля вы получаете его исходный код, и этот код компилируется динамически. Если вы вносите изменения в файл исходного кода, изменения автоматически подхватываются и модуль компилируется заново.

Встраивание зависимостей

До сих пор я уделял внимание одному типу расширения — системе типов, потому что она представляет большинство модулей Orchard, но есть множество других точек расширения — даже слишком много, чтобы я мог перечислить их здесь. А мне еще нужно рассказать об общих принципах, действующих в рамках всей инфраструктуры.

Один из важнейших моментов в высокомодульной системе — свободное связывание. В Orchard почти все, что размещается над низкоуровневой инфраструктурой, представляет собой модули. Даже управление модулями осуществляется с помощью других модулей! Если нужно, чтобы эти модули работали максимально независимо друг от друга и чтобы была возможность заменять реализацию какой-либо функции, вводить жесткие зависимости нельзя.

Главный способ достижения этой цели — использование встраивания зависимостей (dependency injection). Когда вам нужно задействовать сервисы из другого класса, вы не просто создаете его экземпляр, так как это привело бы к установлению жесткой зависимости от этого класса. Вместо этого вы встраиваете интерфейс, реализуемый этим классом, как параметр конструктора (рис. 6).

Рис. 6. Встраивание зависимостей через параметры конструктора

private readonly IRepository<ContentTagRecord>
  _contentTagRepository;
private readonly IContentManager _contentManager;
private readonly ICacheManager _cacheManager;
private readonly ISignals _signals;

public TagCloudService(
  IRepository<ContentTagRecord> contentTagRepository,
  IContentManager contentManager,
  ICacheManager cacheManager,
  ISignals signals)
  _contentTagRepository = contentTagRepository;
  _contentManager = contentManager;
  _cacheManager = cacheManager;
  _signals = signals;
}

Тем самым вы получаете зависимость от интерфейса, а не от класса, и реализацию можно заменять без модификации вашего кода. Конкретная реализация встраиваемого интерфейса больше не является решением потребителя этого интерфейса. Здесь управление инвертируется, и решение передается на сторону инфраструктуры.

Конечно, этот вариант не ограничен интерфейсами, определенными в Orchard. Любой модуль может предоставлять собственные точки расширения, просто объявляя интерфейс, производный от IDependency. На самом деле все вот так просто.

Некоторые другие точки расширения

Я лишь поверхностно обрисовал возможности расширения. В Orchard много интерфейсов, позволяющих расширять систему самыми разнообразными способами. Кто-то мог бы даже сказать, что Orchard — фактически не что иное, как механизм расширения. Все части системы заменяемы и расширяемы.

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

  • IWorkContextAccessor позволяет вашему коду обращаться к контексту работы для текущего запроса. Контекст работы (work context) в свою очередь обеспечивает доступ к HttpContext, текущей разметке, конфигурации сайта, пользователю, теме и культуре. Он также предоставляет средства для получения реализации интерфейса из тех мест, где встраивание зависимостей невозможно или где вам нужно отложить их встраивание до конструирования сайта.
  • IContentManager предоставляет все, что нужно для запроса и управления элементами контента.
  • IRepository<T> позволяет обращаться к низкоуровневым методам доступа к данным, если использования IContentManager недостаточно.

Кто-то мог бы даже сказать, что Orchard — фактически не что иное, как механизм расширения. Все части системы заменяемы и расширяемы.

  • IShapeTableProvider поддерживает самые разнообразные сценарии манипулирования формами (shapes) «на лету». В основном вы подключаетесь к событиям, связанным с формами, и уже таким путем можете создавать альтернативные формы для использования в определенных ситуациях, преобразовывать формы, добавлять к ним новые члены, перемещать в разметке и т. д.
  • IBackgroundTask, IScheduledTask и IScheduled¬TaskHandler — интерфейсы, используемые при необходимости выполнения отложенных или повторяемых задач в фоне.
  • IPermissionsProvider дает возможность вашим модулям предоставлять свои разрешения.

Где узнать больше

Orchard — огромный центр расширения, и описать в одной статье все, на что он способен, — задача невыполнимая. Я лишь надеюсь, что сумел пробудить в вас желание узнать больше об этой системе и самостоятельно изучить ее детали. У нас очень активное и дружелюбно настроенное сообщество на форуме orchard.codeplex.com/discussions, где вам с удовольствием помогут и ответят на ваши вопросы.


Бертран Ле Руа (Bertrand Le Roy) начал карьеру профессионального разработчика в 1982 г., когда опубликовал свою первую видеоигру. В 2002 г. он выпустил, по-видимому, первую CMS, способную работать в ASP.NET. Годом позже его пригласили работать в группе Microsoft ASP.NET, и он переехал в США. Работал над ASP.NET версий от 2.0 до 4 и над ASP.NET AJAX; внес свой вклад в принятие jQuery в качестве официальной части инструментария .NET-разработчиков. Представляет Microsoft в координационном комитете OpenAjax Alliance.

Выражаю благодарность за рецензирование статьи эксперту: Себастьяну Росу (SebastienRos).