Pull to refresh

Orchard CMS для разработчика

Reading time 12 min
Views 20K

На прямо сейчас проходящей в Лас-Вегасе конференции MIX11 была представлена новая версия Orchard CMS – open-source CMS от Microsoft включающая все самое вкусное и полезное из технологий MSFT, находящаяся, можно сказать, на острие. После первого взгляда – CMS вполне понравилась (сравниваю, в основном, с Umbraco) – и появилось желание разобраться — как же собственно происходит разработка для Orchard. Если в этом ваши желания совпадают с моими — читайте дальше.

Подготовка


Итак, приготовим отличный коктейль из приятного, полезного и интересного — и сделаем модуль для Orchard CMS, который позволит для любого сайта добавлять вкусность в Internet Explorer 9 — а именно, закреплять сайт в таскбаре (pin) и добавлять туда списки быстрого перехода (jump lists).
Начнем с подготовки рабочего места. Из инструментов потребуются:
1) Orchard CMS – orchard.codeplex.com
2) Internet Explorer 9 — www.beautyoftheweb.com
3) Windows 7
4) Visual Studio 2010
Orchard можно забрать как в виде скомпиленного пакета, так и в виде исходных кодов. Я рекомендую последнее, т.к. пока документация весьма скромная и часто читать приходится код. Чтобы быстрее втянуться в Orchard просто необходимы к прочтению статьи от XaocCPS [1], в качестве бонуса можно прочитать статьи по разработке с официального сайта [2]. Чтобы поближе узнать, что такое Pinned sites в Internet explorer 9 можно обратиться к MSDN [3] или хорошим статьям [4].

Создание модуля


Итак, сначала создадим заготовку модуля. После быстрой и удобной установки CMS через Web Platform Installer [5] или сборки из исходного кода, открываем cmd.exe, переходим в <orchard_folder>\bin и запускаем orchard.exe. Через пару секунд инициализации система любезно приглашает нас к действию (“orchard> “). Функционал командной строки достаточно широк и включает в себя – управление модулями и фичами, управление контентом, кодогенерацию и миграции, управление пакетами и многое другое. Чтобы оценить возможности можно ввести в приглашении help commands и подивиться могуществу orchard.exe. Но минуты восторга уже позади, пора приступать к кропотливому труду. Разработку модуля начнем с создания его структуры, поэтому в строке приглашения вводим
orchard> codegen module Orchard.IE9Pins /IncludeInSolution:true.
В ответ CMS создаст пустой проект нового модуля и включит его (или не включит, зависит от IncludeInSolution) в общий Solution всей CMS, в папку Modules (разумеется, если вы используете версию из исходников). В общем и целом, проект пустого модуля Orchard до ужаса напоминает проект ASP.NET MVC (коим, по сути, и является). Чтобы окончательно убедить Visual Studio что мы работаем с MVC, можно заменить в файл Orchard.IE9pins.csproj
{349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}
на:
{E53F8FEA-EAE0-44A6-8774-FFD645390401};{349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}.
Это даст возможность использовать приятные возможности студии по работе с MVC – команды типа Add view, Go to view, Add controller и т.п. [6].

Типы контента


Проект создан и готов к работе, пора разрабатывать структуру сущностей для нашего модуля Pinned sites. Как вы уже (надеюсь) узнали из статей XaocCPS основное понятие Orchard – Content Type и Content Item. Каждый Item может включать в себя несколько частей (parts). Для хранения данных о частях используются записи (records), так вот все просто. Определимся со структурой данных для Jump lists. Что же представляет собой это список быстрого перехода? Собственно, это набор записей вида Имя – Ссылка – Иконка. Значит, нам потребуются: запись в DB с тремя полями для хранения информации, Content Part и Content Type. Запись – это класс, унаследованый от ContenPartRecord, причем все свойства, которые объявлятся должны быть виртуальными. Итак, в папке Models проекта создаем файл JumpListItemRecord.cs и помещаем туда класс для записи:
public class JumpListItemRecord : ContentPartRecord
  {
    public virtual string Name { get; set; }
    public virtual string Url { get; set; }
    public virtual string IconUrl { get; set; }
  }


* This source code was highlighted with Source Code Highlighter.

Теперь на основании записи можно определить часть:
public class JumpListItemPart : ContentPart<JumpListItemRecord>
{
  [Required]
  public string Name
  {
    get { return Record.Name; }
    set { Record.Name = value; }
  }

  [Required]
  public string Url
  {
    get { return Record.Url; }
    set { Record.Url = value; }
  }

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


* This source code was highlighted with Source Code Highlighter.

Для проектирования части доступны атрибуты для описания валидации данных (Required, StringLength и т.п.) и, как вы уже заметили, в качестве значений части выступают просто данные лежащей в основе записи. Соберем проект, убедимся что модуль скомпилировался без ошибкок. И теперь пришла пора переходить к вкусностям CMS.

Миграции


Возвращемся в orchard.exe и выполняем команду:
orchard> codegen datamigration Orchard.IE9pins
Да, можно уже шумно выражать восторг и радость – в Orchard введена и реализована концепция миграций. И не надо говорить «А у нас в RoR давно уже есть», в RoR вообще много ништяков, не ломайте кайф. Итак, команда сгенрит файл Migration.cs следующего содержания:
public class Migrations : DataMigrationImpl {

    public int Create() {
      // Creating table JumpListItemRecord
      SchemaBuilder.CreateTable("JumpListItemRecord", table => table
        .ContentPartRecord()
        .Column("Name", DbType.String)
        .Column("Url", DbType.String)
        .Column("IconUrl", DbType.String)
      );      

      return 1;
    }
}

* This source code was highlighted with Source Code Highlighter.

Как видите, все очень просто – для определенной выше записи создается таблица в БД – и на этом работа миграции завершается. Самый наблюдательный увидит, что функция возвращает 1 – и это вовсе не код ошибки. Это номер версии. Ваши самые смелые мечты сбываются – Orchard умеет автоматически определять установленную версию, проверять модуль на обновления – и обновлять. Как это работает – мы увидим чуть дальше, пока пришло время подумать о последней части нашей структуры данных для Jump list – тип содержимого (content type). Определять его самое время (и место) на этапе выполнения миграции – создания или обновления модуля. Смело добавляем перед return 1 следующие строки для создания типа:
ContentDefinitionManager.AlterTypeDefinition("JumpListItem",
        cfg => cfg
          .WithPart("JumpListItemPart")
        );


* This source code was highlighted with Source Code Highlighter.

Читается все достаточно просто – это команда системе создать тип содержимого JumpListItem с единственной частью JumpListItemPart.

Обработчик и драйвер


Перед тем же, как перейдем к интерфейсу для управления частью небходимо добавить две нужные запчасти «под капотом» — handler и driver. Обработчик (handler) нужен, чтобы наша часть могла реагировать на события контента – типа публикации, удаления, версионирования – и, в случае необходимости, предпринимать какие-то действия. Обработчик (идеологически) служит для работы с моделью и не касается представления. Пока же от работы с моделью нам нужно одно – связать часть и запись вместе. Для этого в конструкторе обработчика скажем системе, что в качестве хранилища (Storage) будет использован IRepository JumpListItemRecord’ов (который, в свою очередь, система за счет IoC контейнера привяжет к базе – но тут нашего вмешательства не требуется):
public class JumpListPartHandler : ContentHandler
  {
    public JumpListPartHandler(IRepository<JumpListItemRecord> repo)
    {
      Filters.Add(StorageFilter.For(repo));
    }
  }


* This source code was highlighted with Source Code Highlighter.

В противоположность обработчику, драйвер (driver) работает не с моделью, но с представлением, т.е. управляет рендерингом части. В нашем унаследованном от ContentPartDriver классе можно переопределить ряд функций, нас интересуют две из них – overload функции Editor (одна из них служит для обработки GET запроса, т.е. для вывода, другая – для POST, т.е. для обработки введенных данных). Как, собственно, очевидно из название – методы определяют правила рендеринга и обработки части в режиме редактирования.
public class JumpListItemPartDriver : ContentPartDriver<Models.JumpListItemPart>
  {
    protected override DriverResult Editor(Models.JumpListItemPart part, dynamic shapeHelper)
    {    
      return ContentShape("Parts_JumpListItem_Edit",
        () => shapeHelper.EditorTemplate(
          TemplateName: "Parts/JumpListItem",
          Model: part,
          Prefix: Prefix));

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


* This source code was highlighted with Source Code Highlighter.

Могу понять вашу перву реакцию – «ОМГ, что это?». На самом деле, все не так страшно. Ключевое понятие здесь это ContentShape.

Формы (shapes)


ContentShape – связующее звено между данными части и ее представлением для пользователя, главный посредник на пути к View. Чтобы прояснить детали, рекомендую обратиться к статье XaocCPS. Итак, часть обменивается, собственно, со слоем представления отдавая Shape. В данном случае, мы будем строить форму (shape) для редактора используя в качестве шаблона (TemplateName: “Parts/JumpListItem”) JumpListItem.cshtml из папки Parts и передадим туда в качестве модели нашу часть – JumpListItemPart. С обновлением все еще проще, используем лишь переданный в качестве параметра объект IUpdateModel, который позаботиться как нужно об обновлении части. Теперь система знает, какой шаблон нужно выводить при редактировании части и как обновлять ее (части) содержимое. Сам шаблон JumpListItem.cshtml небходимо поместить в папку Views/EditorTemplates/Parts/. Содержимое шаблона будет весьма тривиально:
@model Orchard.IE9pins.Models.JumpListItemPart
<fieldset>
  @Html.LabelFor(m => m.Name)
  @Html.TextBoxFor(m => m.Name, T("Description"))  
  @Html.LabelFor(m => m.Url)
  @Html.TextBoxFor(m => m.Url, T("Url"))
  @Html.LabelFor(m => m.IconUrl)
  @Html.TextBoxFor(m => m.IconUrl, T("IconUrl"))
</fieldset>


* This source code was highlighted with Source Code Highlighter.

Как видите, это обычный файл разметки Razor использующий типизированную модель JumpListItemPart. Казалось бы, ну уже все, ну хочется же посмотреть, что получилось! Буквально еще минуту терпения – для каждой формы нужно определить, как и где она будет выводится. Используется для этого файл Placement.info в корне проекта. Для нас он будет весьма тривиален:
<Placement>
  <Place Parts_JumpListItem_Edit="Content:1"/>
</Placement>

* This source code was highlighted with Source Code Highlighter.

Для формы, определенной в методе драйвера для нашей части (О_о) определяем, что она будет располагатся в секции Content на позиции 1 (порядковые номера здесь, кстати, не целые, чтобы можно было пристроить свою часть где-нибудь между существующими). В нашем случае, когда тип контента все равно содержит только одну часть, можем выводить где угодно.

Главное меню


Все, части созданы, типа контента созданы – вроде бы уже и «ура»? Еще нет, последний рывок. Понимаю, что это кажется нереально длинным и сложным процессом – но пока большую часть операций можно было сделать даже шаблоном проекта в Студии, это в основном все рутина и вполне автоматизируемая – но для обучения рутина самое то. Все, ободрения на этом закончены, возвращаемся в Студию и делаем последний рывок. Разумеется, мы хотим чтобы наш модуль появился самостоятельным пунктом в главном меню админки – и редактировалось как и обычный контент внутри CMS. Чтобы сообщить системе, что мы хотим наследить в главном меню, всего лишь нужно добавить реализацию интерфейса INavigationProvider. Самый важный метод – GetNavigation, там, собственно, и будет описано создание пункта в меню. В качестве одного из параметров выступает функция (god bless functional programming!) BuildMenu которая для пункта меню построит подменю – у нас же далеко идущие планы – настойки внешнего вида Pinned site и, собственно, элементы Jump list’а. Думаю, вы уже заметили тонкость при создании пунтов подменю — item.Action до ужаса напоминает метод Action из MVC – генерация ссылки на action заданного контроллера. Так, собственно, и есть – а раз есть URL значит предоставить системе маршруты роутинга и контроллеры.

Маршруты


Маршруты добавляются имплементацией класса IrouteProvider – можно посмотреть в Route.cs. Маршрут описывается в виде:
Route = new Route(
            "Admin/IE9JumpList/Create",
            new RouteValueDictionary {
                          {"area", "Orchard.IE9pins"},
                          {"controller", "JumpLists"},
                          {"action", "Create"}
                        },
            new RouteValueDictionary(),
            new RouteValueDictionary {
                          {"area", "Orchard.IE9pins"}
                        },
            new MvcRouteHandler())
          }

* This source code was highlighted with Source Code Highlighter.

Все досточно просто читается без пояснений, следует только отметить, что мы используем везде параметр area = Orchard.IE9pins, своего рода namespace для всех URL нашего модуля. В качестве контроллера задан JumpLists – пришло самое время его реализовать.

Контроллер


Тут ничего нового по сравнению с MVC – обычный контроллер и набор методов-действий, возвращающих ActionResult. Из вкусностей только способ, как в контроллер передаются связи на компоненты CMS – Dependecy Injection way, в качестве параметров конструктора указываем нужные интерфейсы – и вуаля, при инстанциировании контроллера в параметрах приедут зарегистрированные компоненты CMS. Просто и красиво. Возвращясь к методу передачи данный в слой представления, обращу внимание так же на строчку
dynamic model = ContentManager.BuildEditor(item);

* This source code was highlighted with Source Code Highlighter.

Здесь описывается модель, которая будет передана в представление – метод BuildEditor как раз сформирует используя метод Editor драйвера для каждой части контента Shape – который и станет моделью. Чтобы вывести shape в представлении достаточно метода Display:
@{ Layout.Title = T("Create jump list item").ToString(); }

@using (Html.BeginFormAntiForgeryPost()) {
  @Html.ValidationSummary()
  @Display(Model)
}

* This source code was highlighted with Source Code Highlighter.

При создании собственно, контента можно наблюдать такую неоднозначную строку:
var item = Services.ContentManager.New<Models.JumpListItemPart>("JumpListItem");

* This source code was highlighted with Source Code Highlighter.

На самом деле, это следует читать «Создать новый item типа контента JumpListItem, найти в нем часть с типом JumpListItemPart и вернуть как результат». Таким образом, в созданном контенте мы получаем для редактирования только созданную нами часть (хотя других там и нет, если честно).
Последний билд, successfully и идем в админку. Там ищем Modules / Features и с замиранием сердца ищем что-то похожее на IE9Pins. Вот теперь «Ура»!

Активируем (Enable) фичу и… о, да – в главном меню появляется пунт Internet Explorer 9!

И там – вполне милый интерфейс для редактирования списка JumpList’ов. Отлично, теперь надо вывести это на сайте… Но это уже в следующей серии. Итак получилось слишком много текста (при моих то попытках все разъяснять, по идее можно бы было раза в 2 сократить).

Итоги


Итак, подведем итог:
1) В админке добавлен новый пункт в котором управляется список нужных нам пунктов списка быстрого перехода.
2) Автоматически генерятся все нужные структуры данных и типа контента.

Получилось слегка затянуто, но я старался объяснить все как можно подробнее, чтобы не просто посмотреть код, а понять как и что работает внутри CMS. В следующей части (она будет меньше) посмотрим как работают Layout’ы, виджеты, как можно выводить контент на страницу сайта, как работают на практике миграции.
Полный исходный код (включая код для второй статьи, разумеется) можно найти на BitBucket. Он не слишком чистый, там отражено много моих метаний и поисков, а на рефакторинг я пока не отвлекался – т.к. делал все в целях изучения.

P.S. Только не нужно делать вывод «Как там все сложно!» — пока просто не достаточно автоматизировано создание структуры проекта, надеюсь, что в дальнейшем все это можно будет делать быстро и легко – но знать внутренности все равно полезно.

Ссылки


[1] Статьи Архитектура Orchard CMS. Концепции безопасности и разработки, Архитектура Orchard CMS. Концепции компоновки, Архитектура Orchard CMS. Основные понятия
[2] Секция Extending Orchard на официальном сайте
[3] Pinned sites на MSDN
[4] Pinned sites в статье kichik
[5] Как создать свой сайт с нуля с помощью Orchard CMS. Часть 1. Введение в Orchard CMS
[6] Enable MVC 3 “Add | View…” to Orchard Code Generated Module projects
Tags:
Hubs:
+10
Comments 7
Comments Comments 7

Articles