В качестве банального примера здесь можно привести форму заказа, где присутствует общая информация о заказе, и плюс список позиций заказа. В интранетах встречается множество вариаций этой задачи, порой довольно специфичных, но я буду использовать именно форму заказа в качестве примера в этой статье, поскольку она всем понятна и ее легко объяснить.
С технической точки зрения, в этой статье я продемонстрирую использование OOTB веб-частей и их связывание друг с другом и с внешними параметрами. Также в процессе создания решения я буду использовать CSR и даже кастомизировать JSGrid.
Варианты решения
Как известно, в InfoPath специально для отображения подчиненных списков уже есть готовый инструмент, и вроде бы чего тут вообще изобретать велосипед!? Однако, с InfoPath обычно возникает слишком много проблем (подробнее можно почитать в моей статье про формы списков в SharePoint 2013), да и вообще InfoPath уже официально "заканчивается", если вдруг кто не в курсе. Но если не InfoPath, то что?На самом деле задача решается достаточно легко средствами SharePoint, без всяких InfoPath и даже без кода. И вариантов решения даже не один, а довольно много. Например, в некоторых случаях можно использовать Document Set'ы, а Стас Выщепан придумал, что можно банально сделать библиотеку с оформленными по шаблону Excel-документами.
Что до меня, я обычно использую вот какой способ:
- Создается отдельный page layout.
- Создается отдельный список "позиций", с lookup-полем, ссылающимся на библиотеку документов настроенную на упомянутый выше page layout.
- На page layout добавляются поля заказа, а также вебчасть CQWP, ссылающаяся на список позиций, с фильтром по вышеупомянутому lookup-полю.
Даже в изначальном виде этот способ отлично работает, хорошо интегрирован в SharePoint, визуален, пригоден для печати (ну, возможно придется добавить пару стилей CSS для @media print).
Главным недостатком этого варианта я считаю печально известное ограничение на максимум в 5000 записей в списке позиций, что автоматически еще больше ограничивает максимум записей в основной библиотеке документов. Если вы захотите использовать предложенный мной способ, обязательно определите ориентировочную быстроту наполнения списка позиций, задокументируйте это число и известите об этом клиента, и если потребуется, подумайте о своевременной архивации этого списка.
По шагам
Как известно, в SharePoint'е можно застопориться на день-другой на любой даже самой простейшей ерунде, поэтому на мой взгляд, описание реализации по шагам всегда должно присутствовать в статьях про SharePoint :)
Поехали:
- Site Settings -> Master pages and page layouts -> Ribbon -> New document -> Page layout
- Создаем тип содержимого. Используем ссылку "Create new content type"; выбираем Parent Content Type = Article Page (из группы Page Layout Content Types), задаем имя, OK.
- Далее нужно скрыть ненужные поля и добавить те поля, которые вам нужны. Будьте аккуратны с такими полями как например поля типов Publishing HTML и Publishing Image. Они обязательно должны добавляться именно на этом шаге, т.е. через Content Type, а не непосредственно в библиотеку документов, иначе возможны проблемы.
- Как только тип содержимого создан, возвращаемся к созданию Page layout, обновляем список типов содержимого, выбираем только что созданный, выбираем название для Page layout, OK -> page layout создан!
- Создаем отдельный publishing сайт
- Конфигурируем этот сайт на использование вновь созданного page layout'а (Site settings -> Page layouts and site templates -> Page layouts -> Pages in this site can only use the following layouts -> выбираем только наш layout):
- Теперь создаем список товаров и список позиций на этом самом отдельном сайте. Надеюсь понятно, почему списка 2:
- список товаров включает в себя все возможные товары
- список позиций содержит ссылки на те товары, которые были выбраны пользователем в заказ. Т.е. по сути дела список позиций это агрегированная таблица, реализующая связь много-ко-многим, и включающая в минимальном варианте 2 колонки: товар и заказ. Чаще всего там также будут дополнительные данные: например, из таблицы товаров может быть подтянута картинка товара, а также не помешает поле с количеством товаров для заказа. Также логично для списка позиций выбрать настройку "отображать только созданные мной записи", и добавить группировку по заказу. Наконец, не забудьте пожалуйста поле Title скрыть (через тип содержимого), и плюс сделать его не обязательным (в настройках поля).
- Списки созданы - давайте займемся донастройкой page layout'а. Открываем SharePoint Designer, переходим на нашу коллекцию сайтов, Files->_catalogs->masterpage-> ищем вновь созданный page layout, и выбираем Edit file in Advanced Mode через контекстное меню.
- Добавляем обычные поля из типа содержимого. Это кстати можно сделать через SharePoint Designer UI: на Ribbon, есть вкладка Insert, и там эти поля можно найти в раскрывающейся кнопке "SharePoint" - правда иногда приходится немного подождать, т.к. список полей загружается какое-то время, и доступен не сразу.
- Далее добавляем собственно самую интересную часть: список позиций. Тут есть два варианта реализации:
- Наиболее простой способ это сделать - это использовать CQWP (Content by Query WebPart), как я упоминал выше. Эта веб-часть не имеет возможности редактирования, но зато списки товаров и позиций могут располагаться на любом сайте в текущей сайт-коллекции, что очень удобно.
- Чтобы сделать более user-friendly интерфейс и обеспечить редактирование "на лету", можно использовать XLV (XsltListViewWebPart). В этом случае списки товаров и позиций должны располагаться на том же сайте, где расположена библиотека документов, настроенная на использование нашего page layout'а. Этот вариант технически сложнее (но и интереснее).
Список позиций через Content Query Web Part
Последовательность действий в этом случае следующая:
- На любой webpart page временно добавляем CQWP. Например можно добавить на форму создания элемента любого списка
- Настраиваем CQWP на список позиций.
- Добавляем фильтр по полю из списка позиций, которое указывает на заказ (например, у меня это поле называется Order). Обычно я делаю так, что lookup-поле "Order" ссылается на Title страницы, но в настройках этого поля отмечаю также колонку ID (при этом автоматически добавляется Dependent lookup поле "Order:ID"): И тогда фильтр нужно применять уже именно на поле "Order:ID". В качестве значения фильтра необходимо указать строку "[PageFieldValue:ID]".
-
При необходимости изменения способа отображения списка, добавляем стиль в ItemStyle.xsl, и соответственным образом меняем настройки CQWP.
Подсматриваем markup получившейся CQWP из SharePoint Designer, и copy+paste этот маркап в page layout. Обычно нужно скопировать тег WpNs0:ContentByQueryWebPart и регистрацию соответствующего префикса, т.е. код <%@ Register TagPrefix="WpNs0" ... %>.
- Для упрощения редактирования, просто добавьте ссылку (target="_blank") на список перед этой вебчастью, обернув ее в EditModePanel. Практика показывает, что клиенты в большинстве случаев вполне спокойно относятся к такому варианту редактирования, особенно если функционал редактирования доступен ограниченному числу лиц.
Список позиций через Xslt List View Web Part
Для реализации этого варианта потребуется немного больше знаний и немного больше ручных действий:
- Во-первых, перейдите в SharePoint Designer, на отдельный сайт который вы создали, и найдите файл Lists/ваш_список/AllItems.aspx. Перейдите к редактированию этого файла и выкопируйте оттуда тег XsltListViewWebPart, а также соответствующий ему серверный Register-тег для TagPrefix="WebPartPages". Скопированную разметку добавьте на page layout.
- Если сейчас вы посмотрите, как выглядит страница созданная на основе нашего page layout'а, там вы увидите представление списка. Наша задача сделать так, чтобы в режиме редактирования XLV автоматически переходил в режим Quick Edit, а в режиме просмотра выглядел примерно как CQWP. Это делается довольно просто.
- Сначала добавьте код для режима редактирования (нужный код я банально вытащил из 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: - Теперь нужно добавить 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>
- Теперь необходимо добавить фильтр по полю 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>
- сначала нужно добавить в page layout контрол NumberField, ссылающийся на ID текущей страницы, который не будет отображаться, но значение из которого мы будем через ParameterBinding передавать в XLV:
После выполнения этих действий, в режиме редактирования моя тестовая страница выглядела вот как (из общих полей я добавлял только поле Comments):
Вроде бы уже здорово, вау, но... на самом деле ничего не работает :)
Точнее, не работает добавление новых записей - добавиться-то они добавятся, но с текущей страницей связаны не будут, и как следствие, если страницу обновить, они с нее пропадут.
Кроме такой вот большой проблемы, есть также и несколько мелких недочетов:
Результат:
Полный код фрагмента, который получился у меня для XLV, я выложил на pastebin: http://pastebin.com/3TJCcc8f
Вроде бы уже здорово, вау, но... на самом деле ничего не работает :)
Точнее, не работает добавление новых записей - добавиться-то они добавятся, но с текущей страницей связаны не будут, и как следствие, если страницу обновить, они с нее пропадут.
Кроме такой вот большой проблемы, есть также и несколько мелких недочетов:
- при попытке сортировки грида выводятся все записи списка, фильтрация по 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 я получил, используя следующий фрагмент кода, добавленный перед тегом со скриптом:
Отмечу лишь, что для того, чтобы все заработало, вам необходимо будет добавить в представление списка колонку "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 дня. В таких случаях важно не буксовать в одном месте, а придумывать и пробовать разные альтернативные варианты - в шарепойнте их всегда очень много.
В общем, удачи вам в ваших решениях, и не стесняйтесь оставлять комментарии, если у вас есть какие-то вопросы или замечания!
Однако, необходимо понимать, что даже несмотря все это, множество ограничений и
В общем, удачи вам в ваших решениях, и не стесняйтесь оставлять комментарии, если у вас есть какие-то вопросы или замечания!
Андрей , спасибо за статью. Скажите, а какой подход если используется sp2010?
ОтветитьУдалитьДенис, CQWP в SP2010 абсолютно такой же, так что первый вариант без изменений.
Удалитькак все в шарике то не просто :)
ОтветитьУдалитьУ нас подход куда лаконичней.
примерная концепция:
1. есть мастер список - Заказы, и связанный по лукапу список Позиций. При этом в обоих случаях форма может быть кастомизирована в InfoPath или по другому, но лишь бы это была веб-часть.
2. мы странице просмотра "Заказа" настриваем через web (обычная страница веб-частей), и добавляем на нее связанный список "Позиций", Табы, JS
2.1 JS - это некое шаманство замещающее NewItem2(url) и и добавляющее в урл ParentID=ИД Заявки. таким образом при создании новой позиции будет открыта страница создания Новой позиции с переданной ей по урл нужными параметрами.
3. На странице создания "Позиция заказа", добавляем некий функ, допустим через JS который берет из URL ParentID и устанавливает его в лукап поле формы позиции.
собственно все. и ни каких бубнов в SPD. по этой концепции мы уже не одно решение сделали.
Пример как вглядит можно посмотреть здесь http://www.sharepointworkshop.ru/solutions/storagesystem
Александр, дак это то же самое. Понятное дело что вместо page layout можно использовать list form, и тогда "шаманство" с SPD не потребуется. Просто дело в том, что обычно такие вещи как форма заказа и подобные ей, клиенты хотят печатать, и поэтому page layout тут подходит лучше.
УдалитьА в остальном подход полностью идентичен, и на самом деле я делал также раньше, пока не раскопал как сделать через XLV (что намного изящнее, на мой взгляд). В моем случае тот самый "JS" просто делает немного другие вещи, а конкретно - кастомизирует JSGrid.
Просто от одной мысли про PageLayout уже хреново. чета на ...лись мы ней вдоволь. с тех пор у нас четкий стереотип, что лучший тип страниц в шарике это страницы веб-частей.
УдалитьНо а если читывать требование печати, то да вариант с PageLayout круче.
Хотя, я бы все же, для этой задачи выбрал бы InfoPath, а когда ему будет аналог мы на него еще посмотрим :) или если совсем не требуется автоматизации тупо ввод, вариант с шаблоном екселя очень даже привлекательный.
Александр, честно говоря не знаю ни одной проблемы из-за Page Layouts. У нас тут вот, редкий проект делается без Page Layout, постоянно их используют - по поводу и без. И ни разу не возникало проблем.
УдалитьСпасибо, Андрей!
ОтветитьУдалитьКак всегда, что-нибудь новое, интересное :)
В своём опыте использую такой же метод, как описал Александр. Только без Табов и для передачи параметра в ссылке использую XSLT.
Единственное, что меня не устраивает, это медленность загрузки страницы при размещении на ней несколько веб-частей со ссылкой на разные списки. Не подскажешь методы борьбы с этим? Шаманить с JS, чтобы загружать веб-части в разное время? Или подгружать в разных Табах?
Как то особых тормозов не замечал, хотя может у нас разные предоставление о тормозах. А если без XSLT, то быстрее? что то мне подсказывает что прблема как раз в XSLT - тот еще тормоз, а если на нескольких веб-частях то деградация гарантирована.
Удалитьеще конечно сам шарик, сетевая среда и т.п. могут быть причиной.
Евгений, у меня лично несколько веб-частей, будь то XLV или CQWP или DFWP, размещенных на одной странице и отображающих разные списки, никогда не вызывали никаких проблем. Рекомендую попробовать тот же вариант на других списках, на другом environment и т.п. Я подозреваю, что-то есть необычное в твоих веб-частях, или же есть какой-то общий код на той странице, который вызывает такие тормоза.
УдалитьРешил всё-таки разобраться с самой медленной страницей. Проблема оказалась с веб-частью DataFormWebPart, которая выводит информацию из связанного источника данных.
УдалитьКроме неё на странице ещё 6 веб-частей (и XLV и DFWP). Так вот без этой веб-части страница грузится примерно 3 секунды, с ней - 25-30.
Думаю проблема в том, что в одном из списков в связанном источнике чуть более 5000 элементов. Хотя на страницу выводятся единицы.
Что делать и как оптимизировать пока не придумал.
Андрей, подскажите, а этот код должен работать, если разместить его на обычной странице веб-частей?
ОтветитьУдалитьДобавил XsltListViewWebPart, как у Вас описано.
Добавил после него скрипт, который переводит в режим редактирования - работает.
Попробовал вместо него добавить скрипт CSR для изменения представления, получаю "TypeError: Unable to get property '0' of undefined or null reference". Удалил.
Добавил следующий блок, из пункта 5. В итоге: возможность добавления столбцов и сортировки пропала, как и должно, а вот столбец не скрывается, хотя в режиме дебага смотрю свойства этого столбца, стоит "isVisible=false".
Так же, при добавлении строк, этот столбец не заполняется, точнее, видно что-то заполняется на 1 секунду и тут же пустеет, в итоге, при сохранении, значения нет.
С чем может быть связанно такое поведение, не подскажете?
С CSR разобрался. остальные вопросы актуальны :(
УдалитьЕвгений, сори за поздний ответ. Вопрос видел, но сразу не было времени, а потом забыл :(
УдалитьПроверь плиз что orderColumnName и currentPageId выставлены правильно. currentPageId должен быть числом (=listitem.ID), а не строкой. Также посмотри код на pastebin, туда я просто выдрал из SPD полностью работающий код, так что если даже где-то в статье что-то теоретически может быть не так, то там точно правильно. Еще можно попробовать увеличить setTimeout со 100 до 500 например.
Андрей, главное, что ответили :)
УдалитьСо скрытием поля разобрался. Оказалось, что оно скрывалось, а затем, во время выполнения jsgrid.UpdateColumns, опять отображалось. Перенёс jsgrid.HideColumn в конец функции и всё стало ОК.
Осталась проблема с авто-заполнением. currentPageId я пока не использую. Для опыта вручную проставил ID элемента из списка подстановки, который существует. Во время добавления записи, указанный элемент появляется в нужном поле (отключил временно его сокрытие, чтобы проследить) и тут же исчезает. Такое ощущение, что какой-то другой скрипт его очищает.
При увеличении таймаута до 500, получаю ошибку скрипта "Unable to get property 'fieldRawDataMap' of undefined or null reference" во время добавления новой записи.
Так. Следующий этап :) Извиняюсь за флуд.
УдалитьДействительно, заполняемое поле очищает какой-то скрипт ПРИ СОХРАНЕНИИ ЗАПИСИ.
У меня в списке было 2 поля: Title и поле подстановки, которое должно заполняться автоматом.
Я заполняю Title, жму Enter, запускается скрипт сохранения записи и тут же очищает только что заполненное автоматом поле.
Если добавить ещё одно поле, то при переходе на него, автоматическое поле заполняется корректно и при сохранении не очищается.
Получается, что нужно куда-то ещё добавить таймер :)
Интересно.
УдалитьЯ постараюсь проверить еще раз свою версию в ближайшие дни. У меня все 100% работало, но вдруг...
А ты потом обычное представление списка открывал, поле точно остается незаполненным? :)
Да, конечно проверял, всё заполнено :)
УдалитьВозможно дело в том, что у тебя только 1 поле.
Удалить"заполняю Title, жму Enter"...
Я проверял немного по-другому: заполняю все поля (а в моем случае их было 2 и все required) и уже потом жму Enter.
Событие SP.JsGrid.EventType.OnEntryRecordPropertyChanged срабатывает когда что-то изменяется в самой нижней строке, т.н. Entry Record Row, которая собственно и нужна для создания новых записей.
Всё, я кажется разобрался в чем там дело.
УдалитьИспользование setTimeout неверно. Оказывается лучше использовать банальный "lock".
http://pastebin.com/kfgEtecf
Ясно, спасибо! :)
УдалитьАндрей, а не удалось ли решить проблему: "при попытке сортировки грида выводятся все записи списка, фильтрация по ID страницы слетает".
ОтветитьУдалитьДело в том, что у меня на странице несколько таких гридов и хотелось бы сделать так, чтобы пользователь мог включать/выключать их быстрое редактирование. При этих манипуляциях, к сожалению, так же слетает фильтрация по ID :(
Здравствуйте, столкнулся с проблемой в 3 пункте, не могли бы более подробно описать его. Что надо делать с полями Publishing HTML и Publishing Image. При попытке присваивания странице этого шаблона я получаю страницу с ошибкой в стиле чтото пошло не так и "Unknown server tag 'SharePointWebControls:NumberField". Я не знаю как добавить поля Publishing HTML и Publishing Image через Content Type и думаю проблема в этом.
ОтветитьУдалитьИзвините за возможно глупый вопрос, но моих знаний этого продукта пока не хватает чтобы понять что я сделал не так.
Так же при создании строчки заполняю Lookup-поле, столкнулся со следующей проблемой:
ОтветитьУдалитьЕсли заполнять только title (то есть вводим title и нажимем enter, не заполняя другие поля), то Lookup-поле не заполняется, точнее оно заполняется но как-то не так (если нажать на него мышкой он пишет object). Если перезагрузить страницу, то запись будет сохранена а вот Lookup-поле будет пустое. Но если заполнить еще хотя бы одно поле, то все будет нормально.
Если заполнить вторую строчку, то Lookup-поле в ней заполнится корректно, а потом сразу же оно заполнится в предыдущей записи (магия :))).
Все последующие заполнения строк будут нормальными, проблема только с первой.
Проблему удалось решить заменой SP.JsGrid.CreateUnvalidatedPropertyUpdate на SP.JsGrid.CreateValidatedPropertyUpdate.
Как-то так, не знаю может кому пригодиться.