четверг, 20 марта 2014 г.

Введение в CSR

(даже если вы уже смотрели доклад, эта статья все равно может оказаться полезной для вас, т.к. некоторые моменты здесь рассмотрены намного более тщательно, и приводится гораздо больше примеров)

CSR - это способ отображения данных в SharePoint 2013, пришедший на смену скоропостижно скончавшемуся XSLT. С помощью CSR отображаются и представления списков, и формы списков, и результаты поиска. Хотя бы поэтому нужно знать, что такое CSR и как он работает (о чем, собственно, я и намерен рассказать в этой статье).

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

Как устроен CSR

CSR - это подсистема отображения данных, главным отличием которой является тот факт, что она клиентская, т.е. реализована целиком на JavaScript. Иными словами, на стороне сервера рендерятся только данные в Json-формате, которые затем с помощью неких JavaScript-функций превращаются в HTML и затем этот HTML вставляется в DOM страницы.


Т.о. CSR представляет собой своеобразный конвертер, который принимает входящие данные, и возвращает HTML-строку на основе этих данных.

Для удобства CSR реализован не в виде одной огромной функции, а в виде множества javascript-функций, каждая из которых рендерит свой собственный кусочек HTML. Эти функции как правило вызывают одна другую, и таким образом как бы вложены друг в друга.

Исследовав исходный код CSR в файле /_layouts/15/clienttemplates.debug.js, я составил полную схему, которая демонстрирует порядок вызова этих функций:


OnPrePrender и OnPostRender отличаются от Render*-функций тем, что являются событиями, срабатывающими соответственно перед процессом генерации HTML и сразу после завершения этого процесса. Эти события служат в основном для расширений, в то время как Render*-функции выполняют основную работу по отображению.

Если взять в качестве примера представление произвольного списка, можно отметить области, которые были срендерены каждой из этих Render*-функций:


Ну что же, пока что довольно просто и логично, неправда ли?

Используем CSR API

Есть два основных способа кастомизации CSR:

  1. Подписывание на события OnPreRender и OnPostRender
  2. Подмена (override) функций-шаблонов Render*.
Технически оба способа доступны через единое API, а именно через функцию

SPClientTemplates.TemplateManager.RegisterTemplateOverrides(options)

Функция RegisterTemplateOverrides принимает в качестве единственного параметра объект, содержащий всю необходимую информацию для кастомизации и имеющий следующую структуру:

  • OnPreRender
  • Templates
    • View
    • Header
    • Body
    • Footer
    • Group
    • Item
    • Field
  • OnPostRender
В полях OnPreRender и OnPostRender можно указать одну функцию или же массив функций. Пример:

SPClientTemplates.TemplateManager.RegisterTemplateOverrides({
  OnPreRender: function() { console.log('CSR OnPreRender'); },
  OnPostRender: [
    function() { console.log('CSR OnPostRender'); },
    function() { alert('CSR OnPostRender'); }
  ]
});

Для полей внутри составного поля Templates можно указывать функцию или строку. В случае строки внутри можно использовать токены, ссылающиеся на JavaScript-контекст:

SPClientTemplates.TemplateManager.RegisterTemplateOverrides({
  Templates: {
    Footer: "Hello world from <#= ctx.ListTitle #>!"
  }
});

Результат работы такой функции будет следующий:


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

Вместо строки-шаблона можно использовать функцию, вот как это выглядит:

SPClientTemplates.TemplateManager.RegisterTemplateOverrides({
  Templates: {
    Footer: function(ctx) {
      return "Hello world from " + ctx.ListTitle + "!";
    }
  }
});

При генерации HTML-строк в CSR очень удобно использовать функцию String.format, которая автоматически уже есть на любой странице SharePoint'а и позволяет делать то же самое, что серверный C#-овский String.Format.

SPClientTemplates.TemplateManager.RegisterTemplateOverrides({
  Templates: {
    Footer: function(ctx) {
      return String.format("Hello world from {0}!", ctx.ListTitle);
    }
  }
});

Обратите внимание, что обработчик должен принимать в качестве параметра объект ctx - это тот самый JSON-объект, который генерируется на стороне сервера. И естественно, возвращает функция обработчика HTML-строку.

Замечание: при регистрации override'а, исходный шаблон будет ЗАМЕНЕН, т.е. больше он вызываться не будет, а вместо него будет запускаться ваш шаблон. Поэтому, если в предыдущем примере заменить Footer на Body, то результат будет такой:


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

Чтобы этого избежать, можно попробовать вместо замены шаблона использовать событие OnPostRender, и заменять/дополнять нужный вам элемент с помощью jQuery или селекторов.

Вызов других шаблонов

Как я уже упоминал, шаблоны CSR как бы вложены друг в друга, потому что друг друга вызывают. Поскольку вам придется подменять эти шаблоны в процессе кастомизаций, возникает естественный вопрос: а как вызывать низлежащие шаблоны?

Оказывается, когда JSON-объект сгенерированный на стороне сервера начинает использоваться в качестве контекстного объекта в CSR, этот объект дополняется всякими полезными при обработке полями и методами. И в частности, в нем можно найти все Render*-функции:



Таким образом, вызов низлежащих функций не представляет никаких проблем. Пример:

SPClientTemplates.TemplateManager.RegisterTemplateOverrides({
  Templates: {
    View: function(ctx) {
      return ctx.RenderHeader() + ctx.RenderBody() + ctx.RenderFooter();
    }
  }
});

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

Куда же нужно положить JavaScript-код с вызовом RegisterTemplateOverrides, чтобы кастомизация заработала? Очень просто: нужно положить этот код на страницу сразу после представления списка.

Представленный выше пример я сделал следующим образом: перешел в режим редактирования страницы, добавил веб-часть Script Editor, и добавил туда вышеозначенный код:



Чтобы код работал из любого места страницы, необходимо обернуть его в широко известный SP.SOD.executeFunc:

SP.SOD.executeFunc("clienttemplates.js", "SPClientTemplates", function() {

  SPClientTemplates.TemplateManager.RegisterTemplateOverrides({
    // ...
  });

});


В этом случае можно подключать хоть в header, и делать это любым способом (через masterpage, через page layout, через script link и т.п.) - все сработает нормально.

Теперь давайте посмотрим, как сделать так, чтобы кастомизировался только нужный нам view (если на странице несколько view разных списков). Для этого в объекте опций предусмотрены 3 дополнительных поля: ViewStyle, ListTemplateType и BaseViewID.

  • ListTemplateType - номер шаблона списка (перечисление ListTemplateType). Например, для того, чтобы наш шаблон срабатывал только на списки типа "Задачи" ("Tasks"), нужно указать ListTemplateType=171:

    SPClientTemplates.TemplateManager.RegisterTemplateOverrides({
      Templates: {
        Footer: function(ctx) {
          return String.format("Hello world from {0}!", ctx.ListTitle);
        }
      },
      ListTemplateType: 171
    });
    
  • ViewStyle - тип представления. Имеет смысл указывать один из существующих типов представления, который можно задать в настройках любого представления списка в SharePoint:
    ID для этих ViewStyle можно легко выдрать из того самого select'а на странице настроек представления:

  • BaseViewID - это значение свойства BaseViewID у View, его можно подсмотреть в PowerShell или в SharePoint Designer.
Используя эти три поля, можно "нацелить" кастомизацию строго на нужный список, чтобы никакие другие списки на странице не были затронуты.

Про JSLink

Я с удивлением замечаю, что JSLink для многих является практически синонимом CSR. Многие статьи про CSR имеют заголовок из серии "Новая фича шарепойнта - JSLink", и дальше повествование идет о CSR... Я считаю своим долгом рассеять эти заблуждения.

Правда заключается в том, что JSLink не имеет никакого отношения к CSR вообще. Они не связаны. Они могут использоваться вместе, но это совершенно необязательно. CSR прекрасно работает без всяких JSLink, как мы видели выше. А JSLink прекрасно работает без CSR-скриптов.

JSLink - это просто способ прицепить некий javascript к некоему объекту SharePoint. Свойство JSLink присутствует у многих объектов SharePoint:
  • SPContentType
  • SPField
  • SPForm (кстати, по непонятным причинам, мне не удалось заставить работать именно SPForm.JSLink, хотя согласно моим исследованиям через dotPeek, все должно прекрасно работать - не знаю в чем было дело, может быть у вас получится?...)
  • SPView
  • XsltListViewWebPart
  • ListFormWebPart
  • ...
Как только мы проставляем это свойство у некоего объекта SharePoint, везде где он появляется на портале, везде будут добавлены соответствующие js-файлы.

Фактически эти js-файлы добавляются через контрол ScriptLink, что кстати говоря гарантирует автоматическое решение проблемы дубликатов - если кто не знал, контрол ScriptLink не позволяет загрузить на страницу дважды файл с одинаковым путем. Т.е. если поместить 2 ScriptLink-контрола на одну страницу и в обоих прописать один и тот же файл, то будет загружен лишь один файл (но сильно не обольщайтесь, дубликаты отслеживаются только по url, а не по контенту).

Внутри свойства JSLink можно использовать некоторые токены и особый синтаксис:
  • Во-первых, можно загружать несколько файлов сразу, используя в качестве разделителя вертикальную черту ("|").
  • Во-вторых, можно использовать токены "~site" и "~sitecollection", которые будут заменены на соответствующие URL.
  • В-третьих, можно использовать "(d)" на конце, чтобы грузить файлы в режиме SOD (Script-On-Demand). Т.е. вместо того, чтобы быть загруженным на страницу сразу, скрипт будет зарегистрирован в SOD системе и будет действительно загружен только в том случае, если кто-то к нему обратиться, используя "SP.SOD.executeFunc" или подобный метод. Внимание! Только скрипты, поддерживающие SOD, могут быть загружены таким способом.
В общем, JSLink - это очень удобный способ загрузки скриптов в SharePoint'е, и безусловно этот способ можно и нужно использовать вместе с CSR. Однако, важно понимать, что JSLink никак не влияет на CSR, и например можно подцепить CSR-скрипт к объекту списка А, и этим скриптом применять кастомизации к совершенно другому списку В....

Также, надеюсь понятно, что через CSR можно грузить далеко не только CSR-скрипты, но и любые сторонние файлы - например, jQuery, knockout.js и т.п.

Заключение

На мой взгляд CSR - это отличная замена XSLT. Огромным преимуществом CSR является то, что CSR базируется на JavaScript. Сейчас столько всего происходит вокруг JavaScript! Буквально каждую неделю появляются какие-нибудь новые классные библиотеки и фреймворки, создаются инструменты, и т.д. Мне кажется, CSR - это отличный шанс привнести современные достижения web-технологий в SharePoint, и начать создавать по-настоящему удобные и качественные решения.

Конечно, как и везде в SharePoint, у CSR есть свои тараканы, и следует быть крайне осторожными в оценке трудозатрат, особенно если вы пока еще мало работали с CSR.

32 комментария:

  1. Андрей, спасибо за статью. Отличная систематизация знаний по CSR.

    ОтветитьУдалить
    Ответы
    1. Большое спасибо за фидбэк - лично для меня это очень мотивирующе :)

      Удалить
  2. Спасибо, Андрей!
    Видео пересматривать каждый раз не удобно, если нужно вспомнить какой-то момент )), а тут всё наглядно расписано.

    ОтветитьУдалить
    Ответы
    1. Это верно! Я вот вообще информацию из видео не перевариваю :) Как по нему навигироваться? :)

      Удалить
  3. Андрей, для SPO работает в схеме списка:

    <Forms>
    <Form Type="DisplayForm" Url="DispForm.aspx" SetupPath="pages\form.aspx" WebPartZoneID="Main" JSLink="~site/Lists/Files/script.js" />
    </Forms>


    Возможно, дело в апдейтах, которые регулярно под SPO выходят. Столкнулись так с багом в REST API на on premise, хорошо решили в SPO проверить :)

    Спасибо за статью! Давно хотел прочитать deep dive в CSR, а не очередной пример, как поменять отображение поля.

    ОтветитьУдалить
    Ответы
    1. Я проверял это очень давно, как минимум год назад, поэтому вполне возможно что исправили. Или может быть я что-то делал чуть-чуть не так :) Такое в шарепойнте сплошь и рядом: шаг вправо, шаг влево - ничо не работает :)

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

      Удалить
  4. Напишите на эту тему книгу!! Я бы прям купил её за почти любые деньги :-)

    ОтветитьУдалить
    Ответы
    1. Книгу врядли :) Но еще пару постов постараюсь написать. Спасибо за фидбэк!

      Удалить
  5. Я дико извиняюсь... Но хоть убейте меня, а я не смог заставить параметр ViewStyle работать так, как вы это здесь описываете. :-( Чтобы не быть голословным, я даже готов раскопать свой проект в Visual Studio, где я пытался использовать ViewStyle, для кастомизации представление списка.

    ОтветитьУдалить
    Ответы
    1. И в чем была проблема?

      Удалить
    2. Я писал об этом в комментариях под постом "Доклад Client Side Rendering". Главная проблема в том, что JS-скрипт из свойства JSLink просто не подгружается на страницу, если для представления списка в параметре "ViewStyle", выбрано любое иное значение отличное от Default.
      Я сейчас занят переустановкой своей разработческой среды. Закончу и попробую повторить свой эксперимент. :-)

      Удалить
    3. Ну дак подгрузи скрипт другим способом. Как объяснено выше, JSLink напрямую не связан с CSR, и хотя его использовать совместно с CSR удобно, но совсем не обязательно.

      Удалить
  6. Андрей, есть вопрос, может сталкивался. При регистрации шаблона отображения если задано поле Fields и OnPostRender, вот так например:
    var fieldTemplate = {
    Templates: {
    Fields: {
    "myfieldinternalname": {
    "NewForm": renderNewFormField,
    "EditForm": renderEditFormField,
    "DisplayForm": renderDispFormField
    }
    }
    },
    OnPostRender: OnPostRender
    };

    SPClientTemplates.TemplateManager.RegisterTemplateOverrides(fieldTemplate);

    OnPostRender срабатывает после отрисовки каждого поля на форме и получается что код внутри OnPostRender отрабатывает по количеству полей как минимум. Поковырял исходники, внутри OnPostRender никакого контекста поля нет. Для поля задать свой отдельный обработчик OnPostRender тоже нельзя. А мириться с такой неэффективностью не хочется. Или может я в чем то не разобрался. В общем вопрос звучит так: можно ли задать OnPostRender для отдельно взятого поля?

    ОтветитьУдалить
    Ответы
    1. Дело в том, что на самом деле для форм списка существует 3 режима работы веб-части ListFormWebPart: Normal, ServerRender и CSRCustomLayout. По умолчанию работает в Normal, и в этом режиме через CSR рендерится не вся форма, а только скажем так внутренности полей. В этом режиме для каждого поля процессинг CSR запускается заново. Именно поэтому OnPostRender срабатывает много раз, и в каждом случае в контексте лежит всего одно поле в ListSchema.

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

      Вообще я постараюсь родить когда-нибудь пост про CSR в формах списков, потому что там все-таки отличия от представлений списков довольно большие.

      Удалить
    2. Очень ждем, особенно интересно применение к формам набора документов.

      Удалить
  7. Так вот как раз и решение: ctx.ListSchema.Field[0].Name. Спасибо за подсказку!
    Я кстати обращал внимание, что режимы разные, только не знал как называются, форм уже много пришлось сделать. Вьюшек то и не делал толком, в основном кастомизация форм. Если я правильно понял, то по умолчанию, если не указано иное веб часть ListFormWebPart работает в режиме Normal и дает возможность изменить отображение только отдельных частей формы, как, например, полей. Если же в качестве шаблона указать CSRListForm, то веб часть переходит в другой режим работы, в котором за генерацию presentation полностью отвечает разработчик. Видимо это как раз и есть CSRCustomLayout. Первый режим проще, но без второго иногда не обойтись. Меня раньше смущало использование режима CSRCustomLayout если нужно было сильно поменять отображение, скажем пары-тройки полей, футер, а остальное оставить как есть. Но совсем недавно обнаружил, что есть методы генерации представления полей по умолчанию. Все они находятся в файле clientforms.js и называются примерно так: SPLookupField_Display, SPLookupField_Edit, SPNodeField_Display, SPCurrencyField_Edit и т.д.

    ОтветитьУдалить
  8. Андрей, очень мало ещё имел дело с CSR, можете подсказать в одном вопросе...

    Есть форма элемента (DispForm), на ней я добавил "дочерний" список через XsltListViewWebPart, отфильтрованный через Lookup-поле.
    Мне необходимо, чтобы при переходе по ссылке "Создать элемент" на дочерней веб-части, в URL предавалось несколько параметров (ID родительского элемента и несколько ID из полей родительского элемента).
    Отредактировать ссылку "Создать элемент" у меня получилось в функции PreRender. Там же я передал параметр ID родительского элемента, взяв его из URL. А вот как быть с остальными параметрами придумать не могу.
    Когда я добавлял дочерний список при помощи DFWP, то реализовывал это тем, что передавал необходимые поля из родительской в дочернюю форму через подключение веб-частей и затем вставлял эти параметры в ссылку в XSLT-преобразовании. Взять параметры веб-части из контекста CSR у меня не получилось.
    Не подскажите каким способом, методом можно решить данный вопрос?

    ОтветитьУдалить
    Ответы
    1. Так и не смог передать необходимые параметры, решил этот вопрос другим методом.

      Удалить
  9. Андрей, ещё вопрос по теме.

    Есть DispForm списка. На нём стандартный элемент ListFormWebPart. По мимо этого, добавляю несколько XsltListViewWebPart с доп. информацией.

    Вопрос, как мне применить CSR-преобразования к ListFormWebPart, чтобы они не касались XsltListViewWebPart?
    Свойств ViewStyle и BaseViewID у этой веб-части нету, а ListTemplateType у всех одинаковый.

    ОтветитьУдалить
    Ответы
    1. В твоем случае скорее всего проблемы нет:
      1) Если заменяешь шаблоны полей - там сразу и указываешь "DisplayForm"
      2) Если хочешь прицепиться на OnPostRender - проверяй в нём самом, if (ctx.FormContext) ...

      В более широком контексте все не просто :(

      Удалить
    2. Спасибо за советы.
      Сделал пока так:
      - Определил шаблон без указания параметров подключения (BaseViewId и т.п.) который необходим для ListFormWebPart.
      - Затем ещё раз определили, но уже с указанием BaseViewId, который указал на всех XsltListViewWebPart и в нём вернул к штатному отображению с помощью штатных функций RenderItems и т.п.

      Удалить
  10. А как быть, если мне нужно подгрузить некоторые модули с помощью require.js перед тем, как рендерить клиентское представление поля?
    Пробую делать так:
    require(['jquery', 'select2', 'dropdowncontrol', 'webservices'], function ($, select2, dropdowncontrol, webservices) {
    SP.SOD.executeFunc("/_layouts/15/clienttemplates.js", "SPClientTemplates", function () {
    SPClientTemplates.TemplateManager.RegisterTemplateOverrides({
    Templates: {
    Fields: {
    'X5TradeNetwork': {
    EditForm: TradeNetworksControl,
    NewForm: TradeNetworksControl
    }
    }
    }
    });
    });
    });

    Но в этом случае RegisterTemplateOverrides вызывается уже когда всё нарисовалось, то есть слишком поздно. Как вызвать его в нужный момент?

    ОтветитьУдалить
  11. Андрей, есть непонятная ситуация с рендерингом.

    Добавляю код в JsLink дефолтного представления списка. Он отрабатывает только после Ctrl+F5 или Ctrl+R. После обычного обновления странички или перехода на неё по ссылке, код не отрабатывает.

    Пробовал включать MDS, делал так: RegisterModuleInit("/SiteAssets/StatusColors.js", myColorTasks);, именуя функцию выше. Но эти действия не дали результата.

    Подскажете, что я упустил?

    ОтветитьУдалить
    Ответы
    1. Вот код, который я постоянно использую и он точно работает, в том числе с включенным MDS: SharePoint CSR file template

      Удалить
  12. у меня все время ctx = undrfined в TradeNetworksControl
    не могу понять в чем дело?



    SPClientTemplates.TemplateManager.RegisterTemplateOverrides({
    Templates: {
    Fields: {
    'X5TradeNetwork': {
    EditForm: TradeNetworksControl,
    NewForm: TradeNetworksControl
    }
    }
    }

    ОтветитьУдалить
  13. Андрей, подскажите, есть ли простая возможность перегруппировать данные при выводе через CSR?
    Есть датасет, сгруппированный по полю Name, нужно сгруппировать по первому слову из Name.
    Функции нельзя использовать в CAML, получается группировку можно реализовывать через XSLT, но тогда нужно код существенно менять или же есть варианты через CSR?

    ОтветитьУдалить
  14. Присоединяюсь к благодарностям и восхищению проделанной Андреем работой!
    И вопрос. У меня представление списка обычное, я настроил чтобы в него выбрасывались нужные мне поля и значения и хочу отрендерить View полностью сам, пробежавшись по ctx.ListData.Row... Все получается легко и качественно, но дважды! Я не могу понять почему View срабатывает дважды. Подключал свой JS файл по разному, через JSLink, через веб часть ScriptEditor, убирал лишние вебчасти со страницы, но результат не меняется - Шаблон View срабатывает дважды. Почему?

    ОтветитьУдалить
    Ответы
    1. Спасибо :)

      Да, я тоже это заметил недавно, на прошлой неделе впервые обратил внимание. Правда, с шаблоном View редко работаю, поэтому не представляю почему такое может быть. Посмотрел сейчас мельком stack trace, но с первого взгляда непонятно что там происходит, нужно глубже копать. Скорее всего есть какое-то отличие в объекте ctx, но идеальный вроде бы на такую кандидатуру ctx.bInitialRender оба раза true, только что проверил... Может быть его надо самому менять?

      Интересно, что несмотря на то что View вызывается дважды, но с OOTB шаблоном View - поля рендерятся только 1 раз.

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

      Удалить
  15. Спасибо, Андрей, за ваш труд.
    за статьи, за видео
    подскажите - какиминструментами лучше пользоваться при разработке CSR?

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

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