Использование xslt при создании сайта на umbraco

Умбрако предоставляет визуальные средства для хранения и правки содержимого сайта. Данные, показываемые посетителю сайта при просмотре конкретной страницы сайта, определяются шаблоном, назначенным соответствующему типу документа. В простейшем случае ­для того, чтобы отобразить хранящуюся в просматриваемом документе информацию достаточно вставить стандартный макрос вывода содержимого поля текущего документа. Например, для вывода поля title:

<umbraco:Item Field="title" runat="server"/>

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

Поскольку структура документов системы поддерживается в виде XML документа, естественным образом существует возможность создавать макросы преобразования содержимого на языке XSLT.

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

Пусть у нас на сайте есть страница «Новости», на которой требуется показать список новостей с аннотациями и предоставить ссылку на страницу с детальной информацией.

В содержимом сайта этой структуре соответствует документ «Новости», у которого допустимыми дочерними элементами являются документы с типом «Новость».

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

Для этого мы создадим новый XSL макрос в разделе «Developer». Начнем с подходящего шаблона «List Sub Pages from Current Page» – список дочерних страниц относительно текущей. Следует оставить включенным чек-бокс на поле «Create Macro» – тогда будет автоматически создан макрос, привязанный к XSLT файлу. Именно вызов макроса и добавляется затем в шаблон страницы для показа новостей.

В результате мы получим следующий документ.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xsl:stylesheet [
    <!ENTITY nbsp "&#x00A0;">
]>
<xsl:stylesheet
      version="1.0"
      xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
      xmlns:msxml="urn:schemas-microsoft-com:xslt"
      xmlns:umbraco.library="urn:umbraco.library" xmlns:Exslt.ExsltCommon="urn:Exslt.ExsltCommon" 
      xmlns:Exslt.ExsltDatesAndTimes="urn:Exslt.ExsltDatesAndTimes" xmlns:Exslt.ExsltMath="urn:Exslt.ExsltMath" 
      xmlns:Exslt.ExsltRegularExpressions="urn:Exslt.ExsltRegularExpressions" 
      xmlns:Exslt.ExsltStrings="urn:Exslt.ExsltStrings" xmlns:Exslt.ExsltSets="urn:Exslt.ExsltSets"
      exclude-result-prefixes="msxml umbraco.library Exslt.ExsltCommon Exslt.ExsltDatesAndTimes Exslt.ExsltMath 
              Exslt.ExsltRegularExpressions Exslt.ExsltStrings Exslt.ExsltSets ">

    <xsl:output method="xml" omit-xml-declaration="yes"/>

    <xsl:param name="currentPage"/>

    <xsl:template match="/">

        <!-- The fun starts here -->
        <ul>
            <xsl:for-each select="$currentPage/* [@isDoc and string(umbracoNaviHide) != '1']">
                <li>
                    <a href="{umbraco.library:NiceUrl(@id)}">
                        <xsl:value-of select="@nodeName"/>
                    </a>
                </li>
            </xsl:for-each>
        </ul>
    </xsl:template>
</xsl:stylesheet>

Как видим, это обычный XSL файл и его задачей является создание HTML для вставки в нужное место нашей веб-страницы. Отметим следующие особенности:

  • Передача параметров в XSL документ
  • Дополнительные библиотеки методов

Передача параметров извне осуществляется двумя способами: при помощи параметров макроса и при помощи параметра currentPage, который дает узел XML, соответствующий текущему документу, для которого мы конструируем наш HTML.

В нашем случае нам достаточно стандартного параметра currentPage для того, чтобы получить результат.

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

При наличии параметров макроса, во время вставки его в код шаблона или в текст содержимого документа, Umbraco покажет окно выбора значений параметров.

Для доступа к значениям параметров макроса из XSL документа чаще всего используется конструкция следующего вида, которая создает переменную со значением соответствующего параметра:

<xsl:variable name=”source” select=”/macro/source”/>

Дополнительные библиотеки, указанные в описаниях пространств имен xmlns, дают нам ряд полезных функций для использования в XSL. Наиболее используемой библиотекой является umbraco.library, которая содержит функции для работы с Umbraco. Например:

umbraco.library:NiceUrl() – дает url к странице сайта, на основе его внутреннего идентификатора.

umbraco.library:ShortDate() ­– позволяет отформатировать дату.

umbraco.library:GetMedia() – дает XML узел медиа-материала по его идентификатору.

На панели инструментов во время работы с текстом XSL документа доступна кнопка «Insert xslt:value-of», открывающая диалоговое окно, где можно посмотреть список методов подключенных библиотек и их параметры.

Вернемся к созданному документу, немного его перепишем, чтобы он соответствовал рекомендациям по разработке XSL – избавимся от for-each. Затем рассмотрим, что в нем находится.

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE xsl:stylesheet [
    <!ENTITY nbsp "&#x00A0;">
]>

<xsl:stylesheet
      version="1.0"
      xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
      xmlns:msxml="urn:schemas-microsoft-com:xslt"
      xmlns:umbraco.library="urn:umbraco.library" xmlns:Exslt.ExsltCommon="urn:Exslt.ExsltCommon" 
      xmlns:Exslt.ExsltDatesAndTimes="urn:Exslt.ExsltDatesAndTimes" xmlns:Exslt.ExsltMath="urn:Exslt.ExsltMath" 
      xmlns:Exslt.ExsltRegularExpressions="urn:Exslt.ExsltRegularExpressions" xmlns:Exslt.ExsltStrings="urn:Exslt.ExsltStrings" 
      xmlns:Exslt.ExsltSets="urn:Exslt.ExsltSets"
      exclude-result-prefixes="msxml umbraco.library Exslt.ExsltCommon Exslt.ExsltDatesAndTimes Exslt.ExsltMath 
            Exslt.ExsltRegularExpressions Exslt.ExsltStrings Exslt.ExsltSets ">

    <xsl:output method="xml" omit-xml-declaration="yes"/>

    <xsl:param name="currentPage"/>

    <xsl:template match="/">
        <!-- The fun starts here -->
        <ul>
            <xsl:apply-templates select="$currentPage/* [@isDoc and string(umbracoNaviHide) != '1']"/>
        </ul>
    </xsl:template>
    <xsl:template match="*">
        <li>
            <a href="{umbraco.library:NiceUrl(@id)}">
                <xsl:value-of select="@nodeName"/>
            </a>
        </li>
    </xsl:template>
</xsl:stylesheet>

Как уже было сказано, значения атрибутов xmlns в начале документа определяют дополнительные подключаемые библиотеки.

Строка

<xsl:param name="currentPage"/>

определяет переменную currentPage, которая соответствует узлу XML документа для запрошенной посетителем сайта страницы.

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

<xsl:apply-templates select="$currentPage/* [@isDoc and string(umbracoNaviHide) != '1']"/>

Подробнее о структуре XML документов Umbraco [здесь].

При показе информации об отдельном узле мы используем umbraco.library:NiceUrl() для получения url целевой страницы и название узла nodeName в качестве текста для ссылки. nodeName – стандартное поле документа Umbraco, в котором содержится его имя, заданное при создании документа. id – идентификатор узла документа.

<a href="{umbraco.library:NiceUrl(@id)}">
                  <xsl:value-of select="@nodeName"/>

Приведенный простой пример вполне жизнеспособен, тем не менее, в реальных проектах часто необходим более сложный вариант кода:

  • упорядоченный вывод новостей
  • более информативный список
  • хранение новостей не в плоском списке, а сгруппированном в подпапки, например, по году или году-месяцу публикации новости
  • постраничный вывод.

Рассмотрим, что требуется для каждого из этих пунктов.

Упорядочивание вывода

Для новостей имеет смысл показ их в порядке, обратном дате их публикации. Используем сортировку при применении шаблонов для отдельных новостей:

<xsl:apply-templates select="$currentPage/* [@isDoc and string(umbracoNaviHide) != '1']">
    <xsl:sort select="@createDate" order="descending" data-type="text"/>
</xsl:apply-templates>

Здесь мы использовали стандартное поле Umbraco createDate, которое заполнено датой создания документа. Сортировка может быть выполнена над текстом, поскольку даты хранятся в виде YYYY-MM-DDTHH:MM:SS.MS.

Более информативный вывод списка

Ссылка с названием документа – обычно не то, что хотелось бы видеть посетителю. Поэтому более реальный шаблон новости следующий:

<xsl:template match="*">
    <li>
        <div class=”title”>
            <xsl:value-of select="title"/>
        </div>
        <div class="date">
            <xsl:value-of select="umbraco.library:ShortDate(@createDate)"/>
        </div>
        <div class="intro">
            <xsl:value-of disable-output-escaping=”yes” select="intro"/>
        </div>
        <a href="{umbraco.library:NiceUrl(@id)}">
            <xsl:value-of select="umbraco.library:GetDictionaryItem('detailsText')"/>
        </a>
    </li>
</xsl:template>

Здесь мы используем поля новости title – текст с полным названием новости, intro –вступительный текст к новости. Метод umbraco.library:ShortDate() – форматирует дату с использованием текущего выбранного для сайта языка. Метод umbraco.library:GetDictionaryItem() – возвращает значение из словаря для текущего выбранного языка. Значения словаря редактируются в соответствующем разделе административной части сайта.

Группировка новостей в папки

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

<xsl:apply-templates select="$currentPage/descendant::News [@isDoc and string(umbracoNaviHide) != '1']">

Постраничный вывод новостей

XSLT не очень хорошо предназначен для постраничного вывода, поскольку требуется формирование цикла. Здесь лучше использовать Razor. Тем не менее, решение есть и оно существенно увеличит код вывода нашего списка.

Добавляем новые параметры в макрос – recordsPerPage – количество новостей на странице, pageNumber – индекс текущей страницы.

В текст документа добавляем определение соответствующих переменных

<xsl:variable name="recordsPerPage">
    <xsl:choose>
        <xsl:when test="string(/macro/recordsPerPage) != ''">
            <xsl:value-of select="/macro/recordsPerPage"/>
        </xsl:when>
        <xsl:otherwise>
            <xsl:value-of select="10"/>
        </xsl:otherwise>
    </xsl:choose>
</xsl:variable>
<xsl:variable name="pageNumber">
    <xsl:choose>
        <xsl:when test="/macro/recordsPerPage &lt;= 1 or string(/macro/recordsPerPage) = '' or string(/macro/recordsPerPage) = 'NaN'">1</xsl:when>
        <xsl:otherwise>
            <xsl:value-of select="/macro/recordsPerPage"/>
        </xsl:otherwise>
    </xsl:choose>
</xsl:variable>

Далее нам требуется посчитать количество страниц для показа

<xsl:variable name="numberOfRecords" select="count($startNode/descendant::News [@isDoc and string(umbracoNaviHide) != '1'])"/>
  <xsl:variable name="totalPages" select="ceiling($numberOfRecords div $recordsPerPage)"/>

Ограничим показ новостей нужной страницей

<xsl:template match="*">
    <xsl:variable name="firstItemNumber" select="$recordsPerPage * number($pageNumber - 1) + 1"/>
    <xsl:if test="position() &gt;= $firstItemNumber and position() &lt;= number($recordsPerPage * number($pageNumber - 1) + $recordsPerPage )">

    </xsl:if>
</xsl:template>

и добавим показ списка страниц

<xsl:template match="/">

    <xsl:if test="$numberOfRecords &gt; $recordsPerPage">
      <xsl:call-template name="pager"/>
    </xsl:if>
</xsl:template>

Осталось только добавить в конец документа XSL-шаблоны для показа номеров страниц и в результате, получим следующий полный код XSL преобразования для показа новостей.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xsl:stylesheet [
    <!ENTITY nbsp "&#x00A0;">
]>

<xsl:stylesheet
      version="1.0"
      xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
      xmlns:msxml="urn:schemas-microsoft-com:xslt"
      xmlns:umbraco.library="urn:umbraco.library" xmlns:Exslt.ExsltCommon="urn:Exslt.ExsltCommon" 
      xmlns:Exslt.ExsltDatesAndTimes="urn:Exslt.ExsltDatesAndTimes" xmlns:Exslt.ExsltMath="urn:Exslt.ExsltMath" 
      xmlns:Exslt.ExsltRegularExpressions="urn:Exslt.ExsltRegularExpressions" 
      xmlns:Exslt.ExsltStrings="urn:Exslt.ExsltStrings" xmlns:Exslt.ExsltSets="urn:Exslt.ExsltSets"
      exclude-result-prefixes="msxml umbraco.library Exslt.ExsltCommon Exslt.ExsltDatesAndTimes Exslt.ExsltMath 
            Exslt.ExsltRegularExpressions Exslt.ExsltStrings Exslt.ExsltSets ">

    <xsl:output method="xml" omit-xml-declaration="yes"/>

    <xsl:param name="currentPage"/>

    <xsl:variable name="recordsPerPage">
        <xsl:choose>
            <xsl:when test="string(/macro/recordsPerPage) != ''">
                <xsl:value-of select="/macro/recordsPerPage"/>
            </xsl:when>
            <xsl:otherwise>
                <xsl:value-of select="10"/>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:variable>

    <xsl:variable name="pageNumber">
        <xsl:choose>
            <xsl:when test="/macro/recordsPerPage &lt;= 1 or string(/macro/recordsPerPage) = '' or string(/macro/recordsPerPage) = 'NaN'">1</xsl:when>
            <xsl:otherwise>
                <xsl:value-of select="/macro/recordsPerPage"/>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:variable>

    <xsl:variable name="numberOfRecords" select="count($currentPage/descendant::News [@isDoc and string(umbracoNaviHide) != '1'])"/>
    <xsl:variable name="totalPages" select="ceiling($numberOfRecords div $recordsPerPage)"/>

    <xsl:template match="/">
        <xsl:apply-templates select="$currentPage/* [@isDoc and string(umbracoNaviHide) != '1']">
            <xsl:sort select="@createDate" order="descending" data-type="text"/>
        </xsl:apply-templates>

        <xsl:if test="$numberOfRecords &gt; $recordsPerPage">
            <xsl:call-template name="pager"/>
        </xsl:if>
    </xsl:template>

    <xsl:template match="*">
        <xsl:variable name="firstItemNumber" select="$recordsPerPage * number($pageNumber - 1) + 1"/>
        <xsl:if test="position() &gt;= $firstItemNumber and position() &lt;= number($recordsPerPage * number($pageNumber - 1) + $recordsPerPage )">
            <li>
                <div class="title">
                <xsl:value-of select="title"/>
            </div>
                <div class="date">
                <xsl:value-of select="umbraco.library:ShortDate(@createDate)"/>
            </div>
                <div class="intro">
                <xsl:value-of disable-output-escaping="yes" select="intro"/>
            </div>
                <a href="{umbraco.library:NiceUrl(@id)}">
                    <xsl:value-of select="umbraco.library:GetDictionaryItem('detailsText')"/>
                </a>
            </li>

        </xsl:if>
    </xsl:template>

  <xsl:template name="pager">
    <div class="pagerContainer">
      <table border="0" cellspacing="0" cellpadding="0">
        <tr>
          <td>
            <div class="pager">
              <a class="arr">
                <xsl:attribute name="href">
                  <xsl:value-of select="umbraco.library:NiceUrl($currentPage/@id)"/>
                  <xsl:text>?page=1</xsl:text>
                </xsl:attribute>
                <xsl:value-of select="umbraco.library:GetDictionaryItem('toFirst')"/>
              </a>
              <a class="arr">
                <xsl:attribute name="href">
                  <xsl:value-of select="umbraco.library:NiceUrl($currentPage/@id)"/>

                  <xsl:text>?page=</xsl:text>
                  <xsl:choose>
                    <xsl:when test="$pageNumber &gt; 1">
                      <xsl:value-of select="$pageNumber - 1"/>
                    </xsl:when>
                    <xsl:otherwise>
                      <xsl:text>1</xsl:text>
                    </xsl:otherwise>
                  </xsl:choose>

                </xsl:attribute>
                <xsl:value-of select="umbraco.library:GetDictionaryItem('pagerBack')"/>
              </a>
              <xsl:call-template name="for.loop">
                <xsl:with-param name="i">1</xsl:with-param>
                <xsl:with-param name="page" select="$pageNumber"></xsl:with-param>
                <xsl:with-param name="count" select="ceiling($numberOfRecords div $recordsPerPage)"></xsl:with-param>
              </xsl:call-template>
              <a class="arr">
                <xsl:attribute name="href">

                  <xsl:value-of select="umbraco.library:NiceUrl($currentPage/@id)"/>

                  <xsl:text>?page=</xsl:text>
                  <xsl:choose>
                    <xsl:when test="$pageNumber &lt; $totalPages">
                      <xsl:value-of select="$pageNumber + 1"/>
                    </xsl:when>
                    <xsl:otherwise>
                      <xsl:value-of select="$totalPages"/>
                    </xsl:otherwise>
                  </xsl:choose>

                </xsl:attribute>
                <xsl:value-of select="umbraco.library:GetDictionaryItem('pagerNext')"/>
              </a>

              <a class="arr">
                <xsl:attribute name="href">

                  <xsl:value-of select="umbraco.library:NiceUrl($currentPage/@id)"/>

                  <xsl:text>?page=</xsl:text>
                  <xsl:value-of select="$totalPages"/>

                </xsl:attribute>
                <xsl:value-of select="umbraco.library:GetDictionaryItem('toLast')"/>
              </a>
            </div>
          </td>
        </tr>
      </table>
    </div>
  </xsl:template>

  <xsl:template name="for.loop">
    <xsl:param name="i"/>
    <xsl:param name="count"/>
    <xsl:param name="page"/>
    <xsl:if test="$i &lt;= $count">
      <xsl:if test="$page != $i">
        <a>
          <xsl:attribute name="href">
            <xsl:value-of select="umbraco.library:NiceUrl($currentPage/@id)"/>

            <xsl:text>?page=</xsl:text>
            <xsl:value-of select="$i"/>
          </xsl:attribute>
          <xsl:value-of select="$i" />
        </a>
      </xsl:if>
      <xsl:if test="$page = $i">
        <span class="selectedPage">
          <xsl:value-of select="$i" />
        </span>
      </xsl:if>
    </xsl:if>
    <xsl:if test="$i &lt;= $count">
      <xsl:call-template name="for.loop">
        <xsl:with-param name="i">
          <xsl:value-of select="$i + 1"/>
        </xsl:with-param>
        <xsl:with-param name="count">
          <xsl:value-of select="$count"/>
        </xsl:with-param>
        <xsl:with-param name="page">
          <xsl:value-of select="$page"/>
        </xsl:with-param>
      </xsl:call-template>
    </xsl:if>
  </xsl:template>
</xsl:stylesheet>

Использование макроса

Отметим, что XSL файл перед исполнением компилируется и кэшируется системой. Исполняется он на движке MSXML, то есть приятной возможностью является, например, добавление определения функции на языке C# прямо в XSL документ. А поскольку результат компилируется и кэшируется, проблем с производительностью это не вызывает.

Наш макрос таким образом готов к использованию. Вызов его из текста шаблона мастер-страницы выглядит следующим образом:

<umbraco:Macro recordsPerPage="10" pageNumber="[@page]" Alias="news" runat="server" />

Здесь параметр для pageNumber в квадратных скобках заменяется при вызове на GET параметр текущей страницы с именем page.