понедельник, 17 октября 2011 г.

SharePoint и XSLT: Объединяем две колонки в одну

Довольно часто возникает такая задача: объединить несколько колонок списка в одну при отображении. Обычно это требуется в следующих случаях:
  1. Когда в списке ну очень много колонок, поэтому при отображении эти данные поневоле приходится группировать
  2. Когда несколько колонок логично было бы объединить в один блок
  3. Иногда в некоторых колонках картинки или длинные текстовые описания, поэтому поля получаются очень разными по высоте, и оттого выглядят некрасиво
  4. Когда часть данных в списке важна при отображении, но присутствует редко. Например, комментарий от руководителя:

В общем, объединение двух колонок в одну при отображении позволяет сэкономить место на экране, улучшить внешний вид, поправить usability, "вписаться" в брэндбук... Это требуется часто, поэтому важно знать, как это можно сделать.

Как видно из скриншота вверху, в качестве примера я решил использовать стандартный список "Tasks", в который добавил одну колонку типа "

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

Эта задача прямо-таки идеальна для XSLT! Любые другие решения потребуют необоснованно больших трудозатрат. Если кто-то знает, как это можно легко и надежно сделать без XSLT - прошу оставить свои идеи в комментариях.

Итак, давайте подумаем, какие изменения нужно будет сделать для того, чтобы воплотить нашу идею в жизнь:
  1. Скрыть столбец "PM's comment" из представления
  2. Отобразить содержимое поля "PM's comment" в той же ячейке, где находится название задачи.
Скрыть столбец - довольно частая задача, и она реализуется средствами XSLT очень просто. Как я рассказывал в предыдущей статье серии, для определения, какой шаблон придется изменять, лучше всего использовать SharePoint Designer.

Открываем SPD, находим наш список, и открываем его представление "All tasks":



Очевидно, что для скрытия колонки нам потребуется скрыть два шаблона: для отображения заголовка этой колонки, и для отображения ячейки данных этой колонки. Довольно логично, что заголовок и ячейка данных отображаются разными шаблонами, не так ли? :)

Хорошо, давайте ткнем на заголовок нашей колонки, и затем в статусной строке выделим элемент <xsl:template>, первый слева от тега <th> (а точнее, он будет отображен как <th.ms-vh2>):



Надеюсь понятно, почему нужно выделять не первый справа шаблон, а немножко пролистать влево: если скрыть только текст заголовка, сама заголовочная ячейка (тег <th>) и соответствующая ей колонка - всё равно останутся видимыми и не исчезнут из таблицы.

Теперь скопируем атрибуты выбранного нами тега <xsl:template>, выбрав в контекстном меню этого элемента Edit Tag...:

Временно сохраним этот код в любом текстовом редакторе. Отлично, а теперь то же самое нужно проделать с любой из ячеек данных нашей колонки - здесь нам потребуется шаблон, первый слева после тега <td>.

Получившиеся два тэга <xsl:template> нужно закрыть, т.к. при копировании из Edit Tag они получаются незакрытыми. А контент в них вставлять не нужно, т.к. мы их скрываем, а не заменяем.

Итак, можете проверить, вот что должно получиться в итоге:

<xsl:template 
    name="FieldRef_header.PM_x0027_s_x0020_comment"
    ddwrt:dvt_mode="header"
    match="FieldRef[@Name='PM_x0027_s_x0020_comment']"
    mode="header"
    ddwrt:ghost="" />
<xsl:template 
    name="FieldRef_printTableCell_EcbAllowed.PM_x0027_s_x0020_comment"
    match="FieldRef[@Name='PM_x0027_s_x0020_comment']"
    mode="printTableCellEcbAllowed"
    ddwrt:dvt_mode="body"
    ddwrt:ghost="" />

Обратите внимание: я удалил строку "hide" из атрибута ddwrt:ghost. Иначе, дизайнер стирал бы эти шаблоны при каждом переключении между вкладками Code и Designer.

Давайте удостоверимся, работает ли это?

Переходим на вкладку Code, отыскиваем XsltListViewWebPart, добавляем тег <Xsl>, в него вставляем стандартный xsl:stylesheet, подключаем через xsl:include файл стандартных преобразований main.xsl, и наконец, вставляем наш код. Напомню, откуда брать код для xsl:stylesheet и другие базовые вещи - я объяснял и подробно показывал в предыдущей статье серии: SharePoint и XSLT: Hello world!

Теперь переключаемся обратно - и видим, что колонка действительно пропала!

Особо отмечу, почему мы должны скрывать эту колонку именно через XSLT (ведь её можно скрыть и через GUI, перейдя в настройки отображения и сняв соответствующую галочку): дело в том, что если колонка не включена в представление, данные этой колонки не будут переданы и в XSLT, и мы просто не сможем получить значения этой колонки из нашего преобразования. А значения-то нам нужны: ведь мы всё-таки собираемся отобразить эту колонку, хоть и в другом месте.

Следующий шаг - изменить отображение колонки с названием ячейки. Для этого воспользуемся трюком с Conditional formatting, который я уже объяснял. Вкратце: выделяем любую ячейку колонки Title, жмем на Ribbon'е Conditional formatting => Format column, прописываем любое условие (например ID='0'), выставляем любой стиль, потом переключаемся во вкладку Code и смотрим, что получилось...

Ага! В тег Xsl добавился новый шаблон.

Этот шаблон идентифицируется атрибутами match="FieldRef[@Name='LinkTitle']" mode="printTableCellEcbAllowed", т.е. отображает ячейку с InternalName равным LinkTitle. Это служебное поле с типом Computed, оно получается из стандартного Title путем добавления ссылки на модальный диалог формы отображения элемента списка.

Шаблон в целом очень простой. Он почти ничего не делает: только выводит тег <td>, добавляет ему несколько атрибутов, и вызывает нижестоящий шаблон (mode="PrintFieldWithECB"). После вызова этого шаблона, мы можем добавить наш собственный код, который будет отображать наше поле PM's comment.

Даже имея самое скудное представление об XSLT, можно догадаться, что отобразить еще одно поле можно еще одним вызовом <xsl:apply-templates>, ведь очевидно, что содержимое поля LinkTitle отображается именно c помощью этого шаблона.

Но как же указать, что нужно отобразить именно поле PM's comment? Прежде всего, нужно получить уникальный идентификатор этого поля, чтобы на него ссылаться. Это очень просто: в листинге наверху вы могли заметить строку PM_x0027_s_x0020_comment - это собственно и есть InternalName нашего поля.

Дальше, изучив имеющийся apply-templates, можно догадаться, что атрибут select="." указывает на текущий обрабатываемый элемент. А текущий обрабатываемый элемент, как известно, задается с помощью атрибута match текущего шаблона. Т.е., сейчаc обрабатывается FieldRef[@Name='LinkTitle'], а нам нужно обработать  FieldRef[@Name='PM_x0027_s_x0020_comment'].

Т.е. согласно правилам XPath, следует передать в apply-templates select="../FieldRef[@Name='PM_x0027_s_x0020_comment']". Давайте попробуем. Вот такой будет код у второго вызова apply-templates:

<xsl:apply-templates select="../FieldRef[@Name='PM_x0027_s_x0020_comment']" mode="PrintFieldWithECB">
    <xsl:with-param name="thisNode" select="$thisNode"/>
</xsl:apply-templates>

Переключаемся во вкладку Design, видим:



Отлично! Почти то, что нужно! Осталось только сделать это всё немного более приятным глазу.

Для этого, давайте заключим наш дополнительный вызов xsl:apply-templates в div, и добавим этому div'у стили. Но постойте! Если мы сделаем div c padding'ом, и пропишем ему еще и цвет фона - получится нехорошо, т.к. этот квадрат будет виден во всех записях, даже там, где поле PM's comment пустое.

Т.е. нам нужен условный вывод тэга. На самом деле это реализуется элементарно в XSLT - в нем есть специальный тег <xsl:if> для таких вещей. Как создавать условие - можно подсмотреть у Conditional Formatting. Помните же, мы задавали условие ID='0'? SharePoint Designer сгенерировал соответствующую строку для этого, её можно найти внутри тега xsl:attribute сразу после тега td. Очевидно, что если заменить в этом сгенерированном условии ID на  PM_x0027_s_x0020_comment, и заменить ='0' на !='', то получится задать проверку на наличие нашего поля.

Вобщем, в итоге у меня получился вот такой код:

<xsl:if test="$thisNode/@PM_x0027_s_x0020_comment != ''">
   <div style="background-color:#FFEEEE; margin: 5px; padding: 4px 10px; font-size: 9px;">
      <xsl:apply-templates select="../FieldRef[@Name='PM_x0027_s_x0020_comment']" mode="PrintFieldWithECB">
          <xsl:with-param name="thisNode" select="$thisNode"/>
      </xsl:apply-templates>
   </div>
</xsl:if>

Конечно, к div нужно подключать css-класс, inline-стили написаны исключительно в качестве примера, для простоты.

Если теперь перейти к просмотру нашего списка в браузере, получим следующую картину:


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

Подводя итог, сегодня мы научились объединять две и более колонок в одну при отображении списка, и это - ценой всего 10 строк XSL-преобразования (остальные строки нам сгенерировал дизайнер).

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

В следующей статье серии рассмотрена вариация этой задачи и реализован способ по отображению колонки в виде дополнительной строки в таблице.

P.S. Если вам понравилась статья, не забудьте подписаться на мой блог :)

15 комментариев:

  1. Замечательная статья! Это лучшее объяснение работы XSLT в SharePoint из того, что я знаю

    ОтветитьУдалить
  2. Дмитрий, спасибо! На самом деле, мне даже самому понравилась собственная статья :)) Надеюсь, продолжение будет не хуже.

    ОтветитьУдалить
  3. Андрей, статья про бест. Я сразу проработал все свои идеи. Только одного не понял, можно ли изменить разметку xslt во вьюве, если у меня все списки сделаны в VS? (т.е. при деплое у меня все перетрется)

    ОтветитьУдалить
  4. ну когда деплоишь список, там есть элемент View, для которого можно проставить XslLink.

    мы собственно тоже деплоим через студию:)
    кстати, следующий пост серии планируется как раз по этой теме - как разворачивать и подцеплять XSLT через VS.

    ОтветитьУдалить
  5. В чем может быть проблема. Все действия сделал, в конструкторе дизайнера вижу результат (правда цвет не поменялся), но сохраняюсь и через браузер не вижу никаких изменений. Лезу опять в код страницы - там все как было до моих изменений.

    И еще не пойму, что это значит - к div нужно подключать css-класс, inline-стили написаны исключительно в качестве примера, для простоты.

    Какие действия я недовыполнил, подскажите.

    ОтветитьУдалить
  6. Влад, привет! Прошу прощения за поздний ответ - неделя выдалась суматошной, сразу ответить не смог физически, а потом забыл:(

    Про отсутствие сохранения изменений - приходит в голову два варианта:
    1. тщательно проверьте, что у вас только один тег <Xsl> в определении XsltListViewWebPart! Возможно, второй был добавлен ранее с помощью кнопки Conditional Formatting или просто кто-то еще добавил.
    2. Возможно, какая-то ошибка закралась в сам Xslt-код. Тщательно выверите свой код: начните с моего варианта 1 в 1: скачайте файл в конце статьи и скопируйте его в тег Xsl, сохранитесь, и проверьте в браузере что этот вариант сработает.

    Про div: обычно стили (т.е. оформление) подключают используя тег class, а сами стили лежат в отдельном css-файле.
    В этом примере я сделал иначе: стили подключил через тег style, т.е. определения стилей, которые должны обычно располагаться в отдельном css-файле - у меня помещены непосредственно в код страницы.
    Так лучше не делать, или если делать - то в качестве ad-hoc, т.е. например при недостатке времени на более правильное решение.

    ОтветитьУдалить
  7. Прекрасный пост, отличный пример для понимания сути Xslt

    ОтветитьУдалить
  8. Привет Андрей, классная статья, очень доходчиво написана.
    только вот у меня не все гладко получилось по твоему примеру,
    вместо конструкции:
    apply-templates select="../FieldRef[@Name='PM_x0027_s_x0020_comment']"
    которая не хотела работать, заработала только конструкция
    apply-templates select="$thisNode/@PM_x0027_s_x0020_comment"
    если у тебя есть предположения по этому поводу?

    ОтветитьУдалить
    Ответы
    1. Alek, спасибо за отзыв!

      Хочу дать некоторые пояснения, которые критичны чтобы понять, что у тебя не получилось и почему.

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

      Данные списка приходят из некого DataSource, а настройки представления - это по сути SPView в XML-виде, с некоторыми дополнениями. В настройки представления также входит описание каждого SPField. SPField-ы описываются с помощью элементов FieldRef.

      Пример входных данных XsltListViewWebPart можно посмотреть в MSDN.

      XSLT-преобразование, как известно, преобразовывает один XML в другой. ОДИН XML, а тут мы видим что у нас два входных XML.

      Дак вот, оказывается, основной разбор идет по XML'у, соответствующему настройкам представления. А второй XML, с данными, фигурирует в шаблонах как параметр $thisNode.

      Отсюда следует, что:
      1. $thisNode/@PM_x0027_s_x0020_comment отдаст значение поля "PM's comment" для текущей обрабатываемой строки.
      2. Текущий элемент (.) может быть разным, в зависимости от того, в каком шаблоне мы находимся. Поэтому XPath-запрос ../FieldRef[@Name='PM_x0027_s_x0020_comment'] может запросто быть ошибочным, и как следствие вернуть пустой набор элементов.

      Поэтому, прежде всего тебе следует убедиться, что ты размещаешь свой код в правильном XSLT-шаблоне. Шаблон должен иметь атрибут match="FieldRef[@Name='PM_x0027_s_x0020_comment']", и атрибут mode="printTableCellEcbAllowed", если делаешь всё четко по статье. В любом случае, должно быть что-то типа match="FieldRef".

      Код, приведенный в статье, 100%-но рабочий.

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

      Удалить
    2. И еще напоминаю, в конце статьи есть ссылка на файл с готовым XSLT-преобразованием, это на случай если ты вдруг не заметил. Там можно взять точный код который использовался при написании статьи. Сначала стоит заставить его заработать, а потом уже приниматься за кастомизации.

      Удалить
  9. Андрей спасибо за быстрый отклик, чувствую, что мне еще долго разбираться)) (я только начал изучать XSLT)

    На работе сейчас изучаю работу с XSLT под web-part (например http://msdn.microsoft.com/ru-ru/library/bb447557.aspx)

    Андрей пиши еще статьи по XSLT, твои "обьяснялки" очень помогают.

    ОтветитьУдалить
  10. Андрей, привет!
    Применила это подход для изменения представления Календаря. Но если вывести календарь как веб-часть на страницу - он отображается вроде как моим представлением, но колонки разъединяются. Прямое указание моего представления, назначение "по умолчанию" - ничего не помогает...
    вот такая досада с календарём (SP2010)

    ОтветитьУдалить
  11. Добрый день!
    Очень понравилась статья. Я новичок и поэтому узнал для себя много нового!

    Но у меня проблема. Я создаю настраиваемый список, настраиваю ему столбцы и теперь хочу создать кастомное представление. Делал всё как было изложено в статье. НО! Когда я копирую шаблоны заголовка и самой ячейки в и перехожу в конструктор вместо представления появляется следующая ошибка:

    Приложению SharePoint Designer не удается визуализировать XSLT в этом представлении данных. Попробуйте отменить изменения или повторно вставить представление данных.Сбой при задании таблицы стилей процессора : 0x80004005 : Named template 'FieldRef_header._x0420__x0430__x0437__x043c__x04' cannot be defined more than once with the same import precedence.

    А всё, видимо, потому что ddwrt:ghost="", а не ddwrt:ghost="hide", как в примере. Поэтому он не скрывается. Я попытался добавить hide через Edit tag, но из этого ничего не вышло.

    Если можете, пожалуйста, подскажите, что делать.

    ОтветитьУдалить
  12. Изменил непосредственно через Код. Проблемы нет.

    ОтветитьУдалить
  13. Правильно я понимаю, что для SP2013 такое сделать уже не получится?

    ОтветитьУдалить

Внимание! Реклама и прочий спам будут беспощадно удаляться.