среда, 22 октября 2014 г.

Своё контекстное меню в JsGrid


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

В прошлом посте я объяснял, как устроена система событий JsGrid, и оказывается, как раз с помощью события SP.JsGrid.EventType.OnRightClick - можно решить задачу с контекстным меню. Помимо события, впрочем, надо еще уметь само контекстное меню создавать. И оказывается, JsGrid имеет API и для этого тоже!

вторник, 21 октября 2014 г.

Система событий JSGrid

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

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

Выделение цветом важных данных… в JSGrid!

Пока дефинишены пишутся, решил написать про некоторые простые методы работы с JSGrid, на примере подсветки данных в гриде.

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

вторник, 14 октября 2014 г.

Документирую SharePoint JSGrid

Тружусь сейчас над TypeScript-дефинишенами для JsGrid. Работа долгая и тяжкая: для каждой-каждой функции в JS файле из 24.5k строк нужно определить типы параметров и возвращаемого значения, понять что эта функция делает, и все это описать. Потратил уже несколько дней, по 5+ часов, и пока еще только в начале пути... Между прочим, даже главный шарепойнтовский файл, SP.debug.js, имеет меньший размер - ~21k строк...

Скриншот процесса:


суббота, 11 октября 2014 г.

Ускоряем SharePoint-разработку

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

Однако, тестировать SharePoint-решения не так-то просто. Их ведь надо еще задеплоить. А пока деплоишь, заснуть можно!... И потом сиди вспоминай, что вообще протестировать-то хотел...

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

Смешно сказать, но я до сих пор вспоминаю SPVisualDev, расширение для Visual Studio для работы с SharePoint 2007. Мгновенное развертывание в 12 hive, отличная поддержка удаленной разработки и отладки, всё удобно, быстро и просто. Этого в SharePoint Developer Tools нет и в помине даже сейчас, в 2014!...

Да, к сожалению, SharePoint с точки зрения скорости разработки сильно далеко от того, что сейчас происходит во "внешнем мире". А происходит, к слову, там, вот что:

Тем временем во "внешнем мире"...

  1. Проект IDE Light Table набрал $300k на Kickstarter. Основная идея проекта - в "живой разработке", когда IDE всегда находится в режиме отладки (!). Т.е. обычного режима не-отладки просто не бывает. Только начали писать код - тут же можно смотреть состояние переменных, и т.д.
  2. Многие современные IDE поддерживают режим "live development". Пример - опенсурсная IDE Brackets. Вот одноминутная демка:

  3. Visual Studio и ASP.Net развивают проект "Browser Link". Идея состоит в том, чтобы инжектить Signal-R скрипт на страницу, что позволяет в дальнейшем этой страницей управлять: релоадить целиком или частями, изменять, и т.д. "Browser Link" можно расширять с помощью Visual Studio extensions. В составе Web Essentials есть несколько крайне интересных расширений такого плана. Скотт Хансельман в этом 4х-минутном видео подробно все это демонстрирует:

  4. Visual Studio "14" CTP 4 по умолчанию использует open source компилятор Roslyn, который имеет расширенные средства для работы с неполными синтаксическими деревьями, благодаря чему им удалось сделать on-fly incremental compilation. Идея в том, чтобы пропустить шаг Build. Т.е. меняем код, сохраняем, переходим в браузер, F5 - и смотрим изменения.
  5. Помимо общеизвестного jsFiddle, появляется все больше и больше других Fiddle'ов, крайне интересных! Например, проект dotNetFiddle не только позволяет писать C# код онлайн, но также предоставляет режим, в котором можно тестировать полномасштабное ASP.Net MVC приложение (выберите Project Type: MVC)!


И т.д., список можно продолжать. Но я думаю вы поняли идею: все осознают, что чем меньше задержка между написанным кодом и увиденным результатом, тем лучше. И к этому идут. Но что можем сделать мы, SharePoint-разработчики?

Оказывается, и в SharePoint тоже можно сделать немало!


Для начала, продолжая мысль из поста про скрытую прелесть provider-hosted Apps, ну как бы все современные вещи можно там использовать без ограничений :) Так что всё что выше описано, нам по большому счету тоже доступно. Надо просто не забыть воспользоваться.

Во-вторых, кратко вернусь к SPVisualDev. Это open source расширение было написано ОДНИМ человеком в 2008 году. Получается, не настолько уж это и сложно?... ;)

Дальше. Давайте подумаем про BrowserLink. Можно ли что-то похожее использовать в SharePoint? Я думаю - да. Ведь BrowserLink это ни что иное, как банальный SignalR. Если заинжектить аналогичный SignalR скрипт в masterpage SharePoint-сайта, то мы получим точно такое же "живое" соединение Visual Studio и SharePoint. Я думаю можно просто выдрать из страницы то, что Visual Studio инжектит, и запихнуть в SharePoint, и оно будет работать. Не успел проверить пока.

Дальше. А что насчет jsFiddle и ASP.Net MVC fiddle? Можно ли сделать собственный SharePoint fiddle? ДА БЕЗ ПРОБЛЕМ! Вот например оцените, проект JSON fiddle:
Опять же. Сделано буквально на коленке, какой-то мужик посидел пару вечеров и написал. Вы, дорогой читатель, тоже могли бы такое написать, даже в одиночку. И вас, дорогие читатели, смею заметить, дофига.

Моя попытка: CamlJs-Console


Вообще, на удивление, многие вещи не настолько сложные, как кажутся. Примерно в середине сентября я начал работать над крайне интересным проектом под названием CamlJs-Console. Это расширение для хрома, позволяющее создавать CamlJs-запросы в режиме live development, очень похожем на пример выше про Brackets. Пишешь - и сразу же видишь результат.

Оказалось, что Chrome Extensions пишутся на HTML+CSS+JavaScript. Очень просто. При желании можно даже создавать Chrome Extension + Web App (или даже SharePoint App) из одного исходного кода...

БЛИН! Я когда только-только сделал самую первую версию у себя на локальной машине в 3 часа ночи :), я сидел тестировал и не мог остановиться. Это настолько отличалось от вот этого "поправил-5 минут деплой", что чувствовалось как магия какая-то. Серьезно :)

Общий вид:



А вот live preview данных из списка:


Мне даже удалось подцепить туда интеллисенс (на основе TypeScript Language Service - ведь как известно, TS является расширением над JS, т.е. любой валидный JS-код является также и валидным TS-кодом):


И более того, мне даже удалось сделать интеллисенс лучше, чем TS-интеллисенс в Visual Studio! Потому что ведь у меня были живые списки. И их поля... :)


Мне кажется, получилось здорово! И заняло каких-то пару недель, причем большую часть времени возился с интеллисенсом.

Преимуществ у Chrome Extension немало: не надо аутентифицироваться, не надо ничего ставить на сайт (в отличие от того же SharePoint App). Можно тестировать и онлайн сайты, и на dev виртуалке, и что особенно ценно - зайти на production и там проверить запрос, на реальных данных (!). Интеграция работает через JSOM, но можно реализовать вариант на веб-сервисах (через SPServices например), и тогда будет работать даже с SharePoint 2007 (сейчас только SP2010 и SP2013).

Что дальше?


Ооооо... у меня полно идей, просто миллион! Можно делать extension'ы наподобие CamlJs - для работы с конкретным API, решения конкретных проблем. Можно делать более широкопрофильные, наподобие JSOM fiddle, но именно в виде Chrome extension. Можно копать в сторону интеграции Visual Studio и SharePoint, создания чего-то похожего на BrowserLink. Можно подумать о гибридных решениях, Visual Studio -> Chrome Extension -> SharePoint.

И это все может значительно улучшить наш процесс работы с SharePoint. Значительно!

Коллега с работы делает сейчас потрясающую штуку, как раз под впечатлением от CamlJs-Console. Крайне легко делается, но возможности открываются потрясающие. Смысл в том, что в Chrome через F12 можно редактировать файлы, подгруженные на страницу. Но конечно эти изменения никуда не сохраняются. Ну а вот он сделал так, что сохраняются, обратно в SharePoint :) И кода там - 50 строк :))). Проект пока на супер-начальной стадии, поэтому если захочется потестировать, тестируйте аккуратно и только на dev-окружении please :) Ссылка вот.

Заключение


Я из всей этой истории главное вот что понял: все в наших руках. Не надо ждать MS или еще кого. Мы сами можем.

P.S. Если у кого есть идеи/мысли на этот счет, с удовольствием поучаствую в обсуждении - кидайте комментарии! :)

понедельник, 22 сентября 2014 г.

Загадочно бесполезный SPList.GetDataTable

Копал SPQuery в dotPeek, и наткнулся на интересный кусок кода:

if (this.m_Query.ConsiderManagedPipe
    && this.m_Query.SafeArrayFlags == null 
    && (this.m_Query.CalendarDate == DateTime.MinValue && !this.m_Query.IncludeMandatoryColumns) 
    && (this.m_Query.ViewFieldsOnly 
        && (this.m_Query.DataTableOptions & SPListGetDataTableOptions.RetrieveLookupIdsOnly) != SPListGetDataTableOptions.None 
        && ((this.m_Query.DataTableOptions & SPListGetDataTableOptions.UseBooleanDataType) != SPListGetDataTableOptions.None 
        && (this.m_Query.DataTableOptions & SPListGetDataTableOptions.UseCalculatedDataType) != SPListGetDataTableOptions.None)) 
    && (!string.IsNullOrEmpty(this.m_Query.ViewFields) 
    && string.IsNullOrEmpty(this.m_Query.ViewAttributes) 
    && (this.m_List.BaseType != SPBaseType.DiscussionBoard && !this.QueryIncludesMultiValueLookup(this.m_Query.ViewFields)) 
    && !this.m_List.HasUniqueScopes))
{
      ULS.SendTraceTag(963012918U, (ULSCatBase) ULSCat.msoulscat_WSS_Database, ULSTraceLevel.Verbose, "SPListItemCollection.EnsureListItemData: Retrieving data through the managed pipe.");
      this.m_bUseManagedPipe = true;
}

Интересно, думаю. Неспроста! Сами посмотрите: условия все такие, логически связанные с performance. Может это какой-нибудь хитрый performance-boost такой?

Ну, начал разбираться...

суббота, 20 сентября 2014 г.

3 простых способа подружить KnockoutJs и SharePoint

Попал на старый проект, SharePoint 2010. Бывает, что тут сделаешь! Спасаюсь от скуки только благодаря KnockoutJs :) Всвязи с чем - этот пост.

KnockoutJs как MVVM фреймворк на самом деле ничем не уступает тому же Angular'у. Единственная вещь, которую нужно очень хорошо изучить и вызубрить в Knockout - это когда скобки () нужны, а когда нет, и как этот ko observable wrapping вообще работает. Преодолев этот порог, больше с Knockout проблем не возникает. Angular конечно помощнее, более фундаментальный, но и всякой магии в нем намного больше, траблшутить крайне сложно, и модель освоения Angular действительно вот такая (проверено на собственном опыте):



Так что, в своих SharePoint-проектах я преимущественно использую именно Knockout. И в этом посте я опишу три простых техники использования Knockout совместно с SharePoint.

пятница, 29 августа 2014 г.

Поток мыслей: про косячников, внешний мир, и скрытую прелесть Apps

Вот не понимаю, как можно в SharePoint'е кого-то называть косячниками? :)

Ошибка в SharePoint'е может возникнуть абсолютно в любом месте. В самом неожиданном! Самые безобидные действия могут повлечь за собой ужасающие последствия...

четверг, 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.

среда, 19 марта 2014 г.

Web scraping с помощью XmlWebPart

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

На всякий случай, Web scraping - это способ интеграции с внешним ресурсом, при котором нужная информация выпарзивается прямо из HTML-кода некой внешней страницы в интернете. Обычно такое требуется, когда внешний сайт не предоставляет нормального API и нет никакой возможности его сделать (на самом деле, по моему опыту, такие ситуации возникают постоянно).

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

воскресенье, 16 марта 2014 г.

Отображение подчиненных списков в SharePoint

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

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

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

Варианты решения

Как известно, в InfoPath специально для отображения подчиненных списков уже есть готовый инструмент, и вроде бы чего тут вообще изобретать велосипед!? Однако, с InfoPath обычно возникает слишком много проблем (подробнее можно почитать в моей статье про формы списков в SharePoint 2013), да и вообще InfoPath уже официально "заканчивается", если вдруг кто не в курсе. Но если не InfoPath, то что?

На самом деле задача решается достаточно легко средствами SharePoint, без всяких InfoPath и даже без кода. И вариантов решения даже не один, а довольно много. Например, в некоторых случаях можно использовать Document Set'ы, а Стас Выщепан придумал, что можно банально сделать библиотеку с оформленными по шаблону Excel-документами.

Что до меня, я обычно использую вот какой способ:

  1. Создается отдельный page layout.
  2. Создается отдельный список "позиций", с lookup-полем, ссылающимся на библиотеку документов настроенную на упомянутый выше page layout.
  3. На page layout добавляются поля заказа, а также вебчасть CQWP, ссылающаяся на список позиций, с фильтром по вышеупомянутому lookup-полю.
Даже в изначальном виде этот способ отлично работает, хорошо интегрирован в SharePoint, визуален, пригоден для печати (ну, возможно придется добавить пару стилей CSS для @media print).

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

По шагам

Как известно, в SharePoint'е можно застопориться на день-другой на любой даже самой простейшей ерунде, поэтому на мой взгляд, описание реализации по шагам всегда должно присутствовать в статьях про SharePoint :)

Поехали:

  1. Site Settings -> Master pages and page layouts -> Ribbon -> New document -> Page layout
  2. Создаем тип содержимого. Используем ссылку "Create new content type"; выбираем Parent Content Type = Article Page (из группы Page Layout Content Types), задаем имя, OK.
  3. Далее нужно скрыть ненужные поля и добавить те поля, которые вам нужны. Будьте аккуратны с такими полями как например поля типов Publishing HTML и Publishing Image. Они обязательно должны добавляться именно на этом шаге, т.е. через Content Type, а не непосредственно в библиотеку документов, иначе возможны проблемы.
  4. Как только тип содержимого создан, возвращаемся к созданию Page layout, обновляем список типов содержимого, выбираем только что созданный, выбираем название для Page layout, OK -> page layout создан!
  5. Создаем отдельный publishing сайт
  6. Конфигурируем этот сайт на использование вновь созданного page layout'а (Site settings -> Page layouts and site templates -> Page layouts -> Pages in this site can only use the following layouts -> выбираем только наш layout):
    Также выставляем page layout по умолчанию в то же значение, и сохраняем настройки.
  7. Теперь создаем список товаров и список позиций на этом самом отдельном сайте. Надеюсь понятно, почему списка 2:
    • список товаров включает в себя все возможные товары
    • список позиций содержит ссылки на те товары, которые были выбраны пользователем в заказ. Т.е. по сути дела список позиций это агрегированная таблица, реализующая связь много-ко-многим, и включающая в минимальном варианте 2 колонки: товар и заказ. Чаще всего там также будут дополнительные данные: например, из таблицы товаров может быть подтянута картинка товара, а также не помешает поле с количеством товаров для заказа. Также логично для списка позиций выбрать настройку "отображать только созданные мной записи", и добавить группировку по заказу. Наконец, не забудьте пожалуйста поле Title скрыть (через тип содержимого), и плюс сделать его не обязательным (в настройках поля).
  8. Списки созданы - давайте займемся донастройкой page layout'а. Открываем SharePoint Designer, переходим на нашу коллекцию сайтов, Files->_catalogs->masterpage-> ищем вновь созданный page layout, и выбираем Edit file in Advanced Mode через контекстное меню.
  9. Добавляем обычные поля из типа содержимого. Это кстати можно сделать через SharePoint Designer UI: на Ribbon, есть вкладка Insert, и там эти поля можно найти в раскрывающейся кнопке "SharePoint" - правда иногда приходится немного подождать, т.к. список полей загружается какое-то время, и доступен не сразу.
  10. Далее добавляем собственно самую интересную часть: список позиций. Тут есть два варианта реализации:
    • Наиболее простой способ это сделать - это использовать CQWP (Content by Query WebPart), как я упоминал выше. Эта веб-часть не имеет возможности редактирования, но зато списки товаров и позиций могут располагаться на любом сайте в текущей сайт-коллекции, что очень удобно.
    • Чтобы сделать более user-friendly интерфейс и обеспечить редактирование "на лету", можно использовать XLV (XsltListViewWebPart). В этом случае списки товаров и позиций должны располагаться на том же сайте, где расположена библиотека документов, настроенная на использование нашего page layout'а. Этот вариант технически сложнее (но и интереснее).

Список позиций через Content Query Web Part

Последовательность действий в этом случае следующая:
  1. На любой webpart page временно добавляем CQWP. Например можно добавить на форму создания элемента любого списка
  2. Настраиваем CQWP на список позиций.
  3. Добавляем фильтр по полю из списка позиций, которое указывает на заказ (например, у меня это поле называется Order). Обычно я делаю так, что lookup-поле "Order" ссылается на Title страницы, но в настройках этого поля отмечаю также колонку ID (при этом автоматически добавляется Dependent lookup поле "Order:ID"):
    И тогда фильтр нужно применять уже именно на поле "Order:ID". В качестве значения фильтра необходимо указать строку "[PageFieldValue:ID]".
  4. При необходимости изменения способа отображения списка, добавляем стиль в ItemStyle.xsl, и соответственным образом меняем настройки CQWP.
    Подсматриваем markup получившейся CQWP из SharePoint Designer, и copy+paste этот маркап в page layout. Обычно нужно скопировать тег WpNs0:ContentByQueryWebPart и регистрацию соответствующего префикса, т.е. код <%@ Register TagPrefix="WpNs0" ... %>.
  5. Для упрощения редактирования, просто добавьте ссылку (target="_blank") на список перед этой вебчастью, обернув ее в EditModePanel. Практика показывает, что клиенты в большинстве случаев вполне спокойно относятся к такому варианту редактирования, особенно если функционал редактирования доступен ограниченному числу лиц.

Список позиций через Xslt List View Web Part

Для реализации этого варианта потребуется немного больше знаний и немного больше ручных действий:
  1. Во-первых, перейдите в SharePoint Designer, на отдельный сайт который вы создали, и найдите файл Lists/ваш_список/AllItems.aspx. Перейдите к редактированию этого файла и выкопируйте оттуда тег XsltListViewWebPart, а также соответствующий ему серверный Register-тег для TagPrefix="WebPartPages". Скопированную разметку добавьте на page layout.
  2. Если сейчас вы посмотрите, как выглядит страница созданная на основе нашего page layout'а, там вы увидите представление списка. Наша задача сделать так, чтобы в режиме редактирования XLV автоматически переходил в режим Quick Edit, а в режиме просмотра выглядел примерно как CQWP. Это делается довольно просто.
  3. Сначала добавьте код для режима редактирования (нужный код я банально вытащил из onclick атрибута стандартной ссылки "edit this list"):
    <PublishingWebControls:EditModePanel runat="server">
        <script type="text/javascript">
            _spBodyOnLoadFunctions.push(function()
            {
                EnsureScriptParams('inplview', 'InitGridFromView', '{YOUR-GUID-HERE}');
            });
        </script>
    </PublishingWebControls:EditModePanel>
    
    Этот код необходимо поместить сразу после XsltListViewWebPart. GUID берется из View Name: 
  4. Теперь нужно добавить CSR-преобразование, которое бы изменяло внешний вид списка. В простейшем случае это преобразование будет выглядеть следующим образом:
    <PublishingWebControls:EditModePanel runat="server" PageDisplayMode="Display" SupressTag="True">
        <script type="text/javascript">
           SP.SOD.executeFunc('clienttemplates.js', 'SPClientTemplates.TemplateManager', function() {
                SPClientTemplates.TemplateManager.RegisterTemplateOverrides({
                    Templates: {
                        View: function (ctx) {
                            return String.format('<div class="cbq-layout-main"><ul class="dfwp-list dfwp-column">{0}</ul></div>', ctx.RenderBody(ctx)); 
                        },
                        Item: function (ctx) {
                            return String.format('<li class="dfwp-item"><div class="item"><div class="link-item">{0}</div></div></li>', ctx.CurrentItem["Item"][0].lookupValue);
                        }
                    }
                })
           })
        </script>
    </PublishingWebControls:EditModePanel>
    
    
  5. Теперь необходимо добавить фильтр по полю Order. Для этого, нужно сделать следующее:
    • сначала нужно добавить в page layout контрол NumberField, ссылающийся на ID текущей страницы, который не будет отображаться, но значение из которого мы будем через ParameterBinding передавать в XLV:
      <div style="display:none;">
         <SharePointWebControls:NumberField ID="PageID" ControlMode="Display" FieldName="1d22ea11-1e32-424e-89ab-9fedbadb6ce1" runat="server"/>
      </div>
      
    • Теперь добавляем ParameterBinding (рядом с уже существующими биндингами внутри тега XsltListViewWebPart):
        <ParameterBinding Name="pageId" Location="Control(PageId,ItemFieldValue)" />
      
    • И наконец, собственно настраиваем фильтр, добавляя в тег Query (внутри View) следующий код:
      <Where><Eq><FieldRef Name="Order" LookupId="TRUE" /><Value Type="Integer">{pageId}</Value></Eq></Where>
      
После выполнения этих действий, в режиме редактирования моя тестовая страница выглядела вот как (из общих полей я добавлял только поле Comments):

Вроде бы уже здорово, вау, но... на самом деле ничего не работает :)

Точнее, не работает добавление новых записей - добавиться-то они добавятся, но с текущей страницей связаны не будут, и как следствие, если страницу обновить, они с нее пропадут.

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

  • при попытке сортировки грида выводятся все записи списка, фильтрация по ID страницы слетает
  • встроенный в грид функционал по добавлению колонок, а также по их фильтрации - явно лишний.
Поскольку XLV в режиме редактирования выводится с помощью JSGrid, для исправления этих проблем понадобятся знания по контролу JSGrid. К сожалению, документации по JSGrid в интернете практически нет, а API там очень большой и разобраться самостоятельно - сложно. Но если все-таки это сделать, все наши проблемы решаются буквально парой десятков строк javascript-кода (этот фрагмент необходимо разместить после XLV):

<PublishingWebControls:EditModePanel runat="server">
    <script type="text/javascript">
        _spBodyOnLoadFunctions.push(function()
        {
            var viewId = '{8571F3CD-C286-4D4D-89AD-D4B2507A9448}';
            var orderColumnName = 'Order';

            g_SPGridInitInfo[viewId].jsInitObj.canUserAddColumn = false;
            g_SPGridInitInfo[viewId].jsInitObj.showAddColumn = false;
            EnsureScriptParams('inplview', 'InitGridFromView', viewId);
            
            SP.SOD.executeFunc('spgantt.js', 'SP.GanttControl', function() {
                var jsGridContainer = $get("spgridcontainer_" + g_SPGridInitInfo[viewId].jsInitObj.qualifier);
                jsGridContainer.jsgrid.HideColumn(orderColumnName);
                var columns = jsGridContainer.jsgrid.GetColumns();
                for (var i in columns)
                {
                    columns[i].isSortable = false;
                    columns[i].isAutoFilterable = false;
                }
                jsGridContainer.jsgrid.UpdateColumns(new SP.JsGrid.ColumnInfoCollection(columns));
                jsGridContainer.jsgrid.AttachEvent(SP.JsGrid.EventType.OnEntryRecordPropertyChanged, function(args) {
                    if (args.fieldKey != orderColumnName)
                    {
                        var update = SP.JsGrid.CreateUnvalidatedPropertyUpdate(args.recordKey,orderColumnName,currentPageId,false);
                        setTimeout(function() {jsGridContainer.jsgrid.UpdateProperties([update], SP.JsGrid.UserAction.UserEdit);}, 100);
                    }
                });
            });
        });
    </script>
</PublishingWebControls:EditModePanel>

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

Отмечу лишь, что для того, чтобы все заработало, вам необходимо будет добавить в представление списка колонку "Order". Как можно заметить выше, приведенный код ее скрывает от пользователя, и заполняет значением currentPageId каждый раз, когда добавляется новая строка. Переменную currentPageId я получил, используя следующий фрагмент кода, добавленный перед тегом со скриптом:

<script type="text/javascript">
    var currentPageId = <SharePointWebControls:NumberField ControlMode="Display" FieldName="1d22ea11-1e32-424e-89ab-9fedbadb6ce1" runat="server"/>;
</script>

Результат:


Полный код фрагмента, который получился у меня для XLV, я выложил на pastebin: http://pastebin.com/3TJCcc8f

Заключение

SharePoint предлагает огромное количество "строительных блоков" и вариантов их интеграции друг с другом. На основе этих средств в последнее время мне удается решать большинство задач по SharePoint всего за несколько часов.

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

В общем, удачи вам в ваших решениях, и не стесняйтесь оставлять комментарии, если у вас есть какие-то вопросы или замечания!