Показаны сообщения с ярлыком SPList. Показать все сообщения
Показаны сообщения с ярлыком SPList. Показать все сообщения

понедельник, 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 такой?

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

воскресенье, 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 дня. В таких случаях важно не буксовать в одном месте, а придумывать и пробовать разные альтернативные варианты - в шарепойнте их всегда очень много.

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

вторник, 23 апреля 2013 г.

Загадочный список Survey

Список типа "Опрос" в SharePoint - очень интересный и особенный. Прежде всего, уникальность его заключается в том, что он позволяет вставлять разделители страниц (Page separator), т.е. по сути разбивать форму создания элемента списка на страницы!

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


Page Separator - это особый тип поля (SPFieldType.PageSeparator), доступный только для списков типа Survey. По сути это обычная колонка списка, не содержащая никаких данных. Если при выводе полей на форму встречается колонка этого типа, это считается окончанием страницы и следующие поля не выводятся. Также для реализации этого механизма используется параметр командной строки "FirstField", который определяет, начиная с какого поля выводить форму:


Более того, переходы на разные страницы можно даже сделать условными с помощью фишки, которая называется "Branching logic":

Т.е. в зависимости от выбора пользователя, можно пропустить следующую страницу опроса, и т.д.

Конечно, список типа "Survey" ориентирован прежде всего на создание опросов. Однако, в некоторых ситуациях его вполне можно использовать и для других бизнес нужд. Живой пример: регистрации на встречи Russian SharePoint User Group на сайте rusug.net всегда были реализованы именно с помощью обычного списка "Survey" (хоть и без разбития на страницы).

Однако, если вы хотите сделать свой список и свою форму на основе Survey, необходимо учитывать следующие основные ограничения этого списка:
  1. Ответить на опрос можно только один раз. Это автоматически означает, что один пользователь может заполнить форму лишь единожды, т.е. создать только 1 элемент списка. Для регистрации на событие это вполне нормально, а вот оформление заказов таким образом уже не сделаешь...
    Update: Спасибо Алексею за комментарий! Оказывается, при создании списка есть дополнительная опция "Allow multiple responses", которая решает эту проблему.
  2. Бэкэнд заточен под создание вопросов и ответов, и везде используется соответствующая терминология. Если вы планируете давать клиенту возможность изменять этот список (добавлять в него поля и т.п.), надо готовиться, что придется пускаться в пространные объяснения, что это там за вопросы и ответы такие :)
  3. Веб-интерфейс настроек списка сильно урезан. Например, через настройки списка в браузере нельзя создавать представления (хотя через SharePoint Designer или программно представления создаются без проблем).
  4. Survey - это, пожалуй, единственный тип списка, у которого до сих пор остался старый тип тулбара, из 2007го SharePoint'а. Интеграция с риббоном отсутствует.
Проверки на тип списка, как водится, захардкожены по всему SharePoint'у :) Поэтому обойти эти ограничения непросто, если вообще возможно.

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

Как устроен Survey List


Оказывается, реализация многостраничности в опросах довольно простая. Она основывается на переопределенном RenderingTemplate. Вы можете найти этот шаблон в файле CONTROLTEMPLATES/DefaultTemplates.ascx, называется SurveyForm:

<SharePoint:RenderingTemplate id="SurveyForm" runat="server">
    <Template>
        <wssuc:ToolBar CssClass="ms-formtoolbar" id="toolBarTbltop" RightButtonSeparator="&amp;#160;" runat="server">
            <Template_RightButtons>
                <SharePoint:NextPageButton runat="server"/>
                <SharePoint:SaveButton Text="<%$Resources:wss,tb_survey_save%>" accesskey="<%$Resources:wss,tb_survey_save_AK%>" runat="server"/>
                <SharePoint:MultiPageGoBackButton runat="server"/>
             </Template_RightButtons>
        </wssuc:ToolBar>
        <SharePoint:FormToolBar runat="server"/>
        <SharePoint:ItemValidationFailedMessage runat="server"/>
        <table class="ms-formtable" style="margin-top: 8px;" border="0" cellpadding="0" cellspacing="0" width="100%">
        <SharePoint:SurveyFieldIterator runat="server"/>
        </table>
        <table cellpadding="0" cellspacing="0" width="100%"><tr><td class="ms-formline"><img src="/_layouts/15/images/blank.gif?rev=23" width='1' height='1' alt="" /></td></tr></table>
        <table cellpadding="0" cellspacing="0" width="100%" style="padding-top: 7px"><tr><td width="100%">
        <SharePoint:ItemHiddenVersion runat="server"/>
        <wssuc:ToolBar CssClass="ms-formtoolbar" id="toolBarTbl" RightButtonSeparator="&amp;#160;" runat="server">
                <Template_Buttons>
                        <SharePoint:InitContentType runat="server"/>
                        <SharePoint:CreatedModifiedInfo runat="server"/>
                </Template_Buttons>
                <Template_RightButtons>
                    <SharePoint:NextPageButton runat="server"/>
                    <SharePoint:SaveButton Text="<%$Resources:wss,tb_survey_save%>" accesskey="<%$Resources:wss,tb_survey_save_AK%>" runat="server"/>
                    <SharePoint:MultiPageGoBackButton runat="server"/>
                </Template_RightButtons>
        </wssuc:ToolBar>
        </td></tr></table>
    </Template>
</SharePoint:RenderingTemplate>

Для многостраничной логики, здесь задействованы контролы, обозначенные жирным. Наиболее интересным на первый взгляд здесь представляется контрол SurveyFieldIterator. Он реализует логику отображения полей на форме. Если подсмотреть на этот класс через рефлектор или его аналог, вы увидите, что логика отображения на удивление простая и даже в некотором роде элегантная (если элегантный говнокод существует - то это как раз он! :) ).

Хорошие новости: SurveyFieldIterator будет работать для любого списка - т.е. довольно легко его задействовать в каком-нибудь другом, кастомном списке. Плохие новости: в контролы NextPageButton и SaveButton захардкожены проверки на шаблон списка (SPListTemplateType.Survey), и использовать их в кастомном списке не выйдет :(

Заменить эти контролы очень непросто, потому что они используют некоторые методы, помеченные как internal (т.е. только через Reflection, чего я стараюсь избегать). Обойтись без них тоже никак - ведь именно кнопка "NextPageButton" обеспечивает промежуточное сохранение элемента списка и реализует кучу другой интересной логики. В общем, после нескольких часов ковыряния в рефлекторе, я так и не смог найти нормального способа сделать многостраничный список в SharePoint таким же способом, как сделан список Survey, т.е. через ListFieldIterator... :(

Помимо невозможности замены NextPageButton, есть кстати и другие ограничения. К примеру, поле PageSeparator через веб-интерфейс доступно только для списков типа Survey. Впрочем, это поле очень легко добавить программно, или например вот таким простейшим PowerShell-скриптом:

$w = get-spweb http://localhost
$l = $w.Lists["My List"]
$l.Fields.Add("PageSeparator", [Microsoft.SharePoint.SPFieldType]::PageSeparator, $false, $false, $null)

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

Branching Logic иногда тоже было бы полезно использовать, но и эта фишка в UI доступна только для списков типа "Опрос". Так что тут снова придется либо действовать PowerShell-ом, либо писать отдельный UI - если требуется, чтобы пользователи сами могли настраивать логику отображения формы (что, кстати, требуется крайне редко).

Также, следует отметить, что стандартные кнопки Save и Cancel работать не должны. Впрочем, убрать их с Ribbon довольно легко следующим Custom Action:

<CustomAction Id="RemoveRibbonButton" Location="CommandUI.Ribbon">
  <CommandUIExtension>
    <CommandUIDefinitions>
     <CommandUIDefinition Location="Ribbon.ListForm.Edit.Commit" />
    </CommandUIDefinitions>
  </CommandUIExtension>
</CustomAction>

На этом пункте, я посчитал свое исследование законченным, и надеюсь результаты этого исследования вам было интересно читать, пусть мне и не удалось реализовать изначальную мою идею :)

Заключение


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

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

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

Что же касается многостраничных форм - реализовывать их в SharePoint сейчас на удивление тяжело - на мой взгляд это в некотором роде прокол со стороны MS, ведь бизнес-кейс это очень частый. Есть у них конечно полуживой InfoPath и малополезный Scenario Framework, но InfoPath - действительно полуживой и полон собственных ограничений и багов, а Scenario Framework - мало что дает по сравнению с обычным ASP.Net-визардом, созданным "вручную". В настоящий момент наиболее перспективным подходом к задаче многостраничных форм мне лично видится использование Client Side Rendering (CSR) из SharePoint 2013 - но это пока что подход не проверенный. Если будет шанс реализовать многостраничную форму на CSR, я этим обязательно с вами поделюсь, а на сегодня у меня все. Удачи!

понедельник, 22 апреля 2013 г.

Атрибуты ViewFields

С первого взгляда, ViewFields - это попросту список полей, которые отображаются в том или ином представлении списка. В C# API этот список полей представлен классом SPViewFieldCollection, который является по сути коллекцией строк, представляющих собой InternalName полей, отображаемых представлением.

Немногие знают, что каждое поле внутри ViewFields (элементы FieldRef) еще может иметь атрибуты, которые определяют, как именно это поле включается в представление списка. Большая часть этих атрибутов присутствует у исходных SPField (однако не все атрибуты SPField переопределяемы!), и установка их в FieldRef позволяет просто переопределить (override) настройки поля в рамках конкретного представления.

Меню элемента


Пожалуй, одни из самых известных и используемых атрибутов элементов ViewFields - это LinkToItem и ListItemMenu. Они позволяют "подцепить" соответственно ссылку на Display форму элемента и меню элемента на произвольное поле списка - без всяких там XSLT преобразований. Нередко эти атрибуты используются, например, в библиотеках документов, где вместо имени файла предпочтительнее отображать заголовок документа (Title) - и соответственно, меню элемента должно быть перенесено на заголовок.

<FieldRef Name="MyField" LinkToItem="TRUE" ListItemMenu="TRUE" />

В SharePoint 2013, к слову, добавился похожий атрибут "CalloutMenu", который позволяет отобразить более современное меню элемента вида "Callout".

Фильтрация и сортировка


Установка атрибутов Filterable и Sortable в FALSE поможет запрещать фильтрацию и сортировку выбранных полей в пределах вашего представления. Дополнительно, можно указать сообщение, которое будет отображаться, когда фильтрация недоступна (атрибут FilterDisableMessage).

Когда это требуется?

  1. Например, вы выносите фильтр по некоторому полю в отдельную панель наверху представления, и улучшаете этот фильтр, делаете его более "user friendly". Скажем, вместо фильтрации по конкретной дате, вы предлагаете пользователю выбрать месяц/год или даже лучше - выбрать конкретный диапазон дат. В этом случае фильтр в заголовке колонки становится ненужным, и почему бы его не запретить.
  2. В некоторых полях фильтрация просто выглядит глупо (яркий пример - поле "Title").
  3. Когда вы используете в запросе представления (тэг <Query>) параметры (значения из ParameterBindings), фильтры SharePoint ломаются, т.е. не выдают вариантов для фильтрации, как будто в колонке совсем нет данных. В этом случае я предпочитаю явно запрещать фильтрацию и определять сообщение об ошибке для всех полей, что-нибудь типа "В этом представлении фильтрация невозможна"...

Explicit


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

Explicit делает две вещи:
  1. Скрывает поле из представления. Т.е. данные для поля будут запрашиваться и поступать в XSLT преобразование, но отображаться соответствующие поля не будут, пока вы их сами не отобразите.
  2. Не дает пользователю удалить поле из представления!!

Те люди, которые используют свойство Sealed у SPField, меня поймут! :)

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

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

SharePoint 2013


В SharePoint 2013, помимо уже упоминавшегося выше CalloutMenu, к ViewFields добавилось немало новых интересных атрибутов. В частности, это атрибуты для управления отображением полей типа "Пользователь": WithPicture, PictureSize, PictureOnly, и некоторые другие.

Например, добавление WithPicture="TRUE" к полю "Created By" (InternalName="Author") выдает вот такой результат:

А вот такой FieldRef:

<FieldRef Name="Author" PictureSize="Size_36px" PictureOnly="TRUE" />

Будет отображен следующим образом:
Таким образом, можно изменять отображение одного и того же поля в зависимости от представления. Это нередко пригождается на практике, чтобы "зафиксировать" режим отображения поля для кастомизированного представления, чтобы вне зависимости от того что пользователь может поменять некоторые настройки для поля через веб-интерфейс, это поле отображалось бы в нашем представлении именно так, как нужно.

Еще один атрибут, специфичный для SP2013, о котором хотелось бы упомянуть - это AllowGridEditing. Он позволяет сделать ту или иную колонку нередактируемой в режиме Grid в рамках данного представления. Выглядит полезно, но будьте осторожны: ограничение редактирования такого рода запретами ненадежно, всегда можно будет изменить это поле куском JS через Client Object Model. О том, как правильно управлять разрешениями и доступом в SharePoint, у меня есть отдельная статья.

Еще раз напомню: все эти атрибуты как правило доступны в виде свойств объекта SPField, т.е. их можно задавать и на уровне поля, для всех представлений сразу.

Другие атрибуты


Есть для элементов ViewFields/FieldRef и другие атрибуты. Например, атрибуты Len и MoreText, нужные для обрезания текста...

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


Иными словами, ссылки, помещенные внутрь текста, становятся кликабельными. Повторюсь, работает это только для обычных текстовых полей.

Как задать атрибуты ViewFields программно?


Как я уже упоминал выше, через SPView.ViewFields задать эти атрибуты нельзя. Что же делать?

Очевидно, решение - это попробовать как-то передать в SPView кусок View XML. К сожалению, напрашивающийся для этого метод SetViewXml попросту не работает - проглатывает любой поданный ему XML, но после Update изменения не сохраняются в БД.

Впрочем, если тщательно пошарить в MSDN, вы обнаружите, что конструктор SPView принимает XmlDocument, что практически то что нужно! Однако, метод Update на созданном таким образом SPView, как ни странно, не работает!... Решение этой проблемы можно найти там же в MSDN, в секции Remarks:

Use the Clone method on the constructed view object to add the view to collection of views for the list.

Кто бы мог подумать! Создаем SPView используя конструктор, а потом его клонируем, и вот тогда уже можно сохранять. Система нипель, блин :)

Вот вам рабочий код:

var doc = new XmlDocument();
doc.LoadXml("<View ...>...</View>"); // or load from file: doc.Load("myview.xml");
var tempView = new SPView(list, doc);
var view = tempView.Clone("My view", 30, true, false);
view.Update();

Заключение

Создание кастомизированного представления списка - это частая потребность. Поэтому важно уметь управлять представлением и его полями в полной мере. Надеюсь, описанные мной здесь, по большей части малоизвестные атрибуты элементов ViewFields помогут сделать ваши представления лучше. Удачи!

четверг, 7 марта 2013 г.

Динамическая форма списка в SharePoint Designer

Как известно, SharePoint Designer (далее SPD) генерит формы списков в “статическом” виде (Lists and libraries –> [ваш список] –> Forms –> New…). На каждое поле списка в XSLT разметке генерится отдельный тэг <tr>, в который захардкожены значения для заголовка и названия поля. Выглядит один такой <tr> следующим образом:
<tr>
    <td width="190px" valign="top" class="ms-formlabel">
        <H3 class="ms-standardheader">
            <nobr>Title<span class="ms-formvalidation"> *</span>
            </nobr>
        </H3>
    </td>
    <td width="400px" valign="top" class="ms-formbody">
        <SharePoint:FormField runat="server" id="FormField1" ControlMode="Edit" FieldName="Title" __designer:bind="{ddwrt:DataBind('u',concat('ff1',$Pos),'Value','ValueChanged','ID',ddwrt:EscapeDelims(string(@ID)),'@Title')}"/>
        <SharePoint:FieldDescription runat="server" id="FieldDescription1" FieldName="Title" ControlMode="Edit"/>
    </td>
</tr>

Таких кусков в XSLT коде столько, сколько у вас на форме полей.

Этот подход приводит к очевидным неприятностям: если в список добавилось новое поле, форму придется перегенерить. Что неприятно: списки они ж ведь на то и списки, чтобы пользователи сами туда могли добавлять поля!…

Оказывается, решить эту проблему – очень легко.

среда, 6 марта 2013 г.

Способы кастомизации форм списков в SharePoint 2013

Я уже писал про формы списков и про то, как их можно кастомизировать, однако в свете новых возможностей SharePoint 2013, а также накопившегося с того времени опыта (прошло уже 1.5 года, как время летит!), хочется еще раз пробежаться по этой теме.

вторник, 26 июня 2012 г.

SharePoint и XSLT: область ссылок «быстрого доступа»

Возвращаюсь к серии статей про SharePoint и XSLT.

Все вы помните ссылку «Добавить новый элемент» внизу представлений списков SharePoint:

image

В связи с наличием этой ссылки возникает два естественных вопроса:
  1. Как поменять текст этой ссылки? (например вместо “Добавить новый элемент”, я хочу текст “Добавить товар” или “Добавить продукт” и т.д.)
  2. Как добавить еще пару ссылок рядом со ссылкой для создания нового элемента?
В последнее время я очень часто и текст ссылки меняю, и другие ссылки добавляю к ней. Такие представления начинают смотреться гораздо приятнее и дружественнее пользователю.

Например, некий список с заявками может иметь вот такие ссылки быстрого доступа:

image

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

В этой статье я расскажу, как реализовать такие изменения. Рассмотрим два способа:
  1. no-code — для частных решений, быстрых правок и кастомизаций Office365
  2. программно – что может потребоваться для создания коробочных решений

вторник, 5 июля 2011 г.

Использование HTML5 в формах списков SharePoint

Недавно я писал про RenderingTemplate и использование перегруженного ListFieldIterator для того, чтобы изменять отображение форм списков SharePoint. В качестве примера использования этого способа, я привел скриншот проекта, где поля списка распределены по вкладкам. Также, в том посте был выложен для скачивания "базовый" проект-пример на эту тему.

Сегодня я хочу еще раз вернуться к RenderingTemplate и ListFieldIterator, рассмотрев их более тщательно и иллюстрированно, на другом примере - внедряя элементы управления HTML5 в формы списков SharePoint.

пятница, 24 июня 2011 г.

Метаданные для SPField

У объекта SPField есть два полезных метода: GetCustomProperty и SetCustomProperty. Но к сожалению, использовать их в качестве обычного PropertyBag очень сложно:
  1. Свойства эти можно использовать только для Custom Field Type'ов, прописывая их названия и типы в схеме типа поля
  2. Вдобавок, нужно еще изменять схему экземпляра поля (SPField.Schema), добавляя туда атрибут Customization. Автоматически это почему-то не делается :(
Естественно, динамически добавлять свойства к уже существующим полям с помощью этих методов никак не получится. Однако, решения есть, и они известны. Дело в том, что схема поля (SPField.Schema) позволяет использовать произвольные атрибуты для элемента Field.

Обычно, для того, чтобы их задействовать, используют private методы класса SPField: SetFieldAttributeValue и GetFieldAttributeValue. Но в этом случае, естественно, используется Reflection, который запрещен в Office365. И вообще, Reflection - это хак, а любые хаки желательно обходить стороной.

Поэтому, я написал собственное решение, которое парзит XML и меняет свойство FieldSchema самостоятельно. Решение получилось довольно простым, и я всё чаще и чаще использую этот самодельный PropertyBag для хранения разных дополнительных атрибутов полей.

Код (не забудьте подключить пространство имен System.Xml.Linq):

  private const string nameSpace = "my";

  /// <summary>
  /// Прикрепляем метаданные к колонке списка. Эти метаданные будут храниться
  /// в схеме поля.
  /// </summary>
  /// <param name="field">Колонка (SPField), к которой следует добавить метаданные</param>
  /// <param name="key">Название поля метаданных</param>
  /// <param name="value">Значение поля метаданных</param>
  public void SetFieldMetaData(SPField field, string key, string value)
  {
   var fieldSchema = XDocument.Parse(field.SchemaXml);
   var tabAttribute = fieldSchema.Element("Field").Attribute(XNamespace.Get(nameSpace) + key);
   if (tabAttribute == null)
    fieldSchema.Element("Field").Add(new XAttribute(XNamespace.Get(nameSpace) + key, value));
   else
    tabAttribute.Value = value;

   field.SchemaXml = fieldSchema.ToString();
   field.Update();
  }

  /// <summary>
  /// Получаем метаданные из колонки.
  /// </summary>
  /// <param name="field">Колонка, из которой считываются метаданные</param>
  /// <param name="key">Название поля метаданных</param>
  /// <returns>Возвращает значение поля метаданных для колонки, или null, если поле метаданных с указанным именем не было ранее присоединено к этой колонке.</returns>
  public string GetFieldMetaData(SPField field, string key)
  {
   var fieldSchema = XDocument.Parse(field.SchemaXml);
   var tabAttribute = fieldSchema.Element("Field").Attribute(XNamespace.Get(nameSpace) + key);
   if (tabAttribute == null)
    return null;
   else
    return tabAttribute.Value;
  }

среда, 15 июня 2011 г.

Как правильно изменять внешний вид List Form в SharePoint

Всё в SharePoint'е можно делать разными способами. Но в последнее время, очень хочется делать правильно...

На этот раз, мне потребовалось внести изменения на формы стандартного SharePoint'овского списка. Таких форм существует три вида, и вы все их прекрасно знаете:
  1. DisplayForm - форма отображения элемента списка
  2. EditForm - форма изменения элемента списка
  3. NewForm - форма создания нового элемента списка
Из-за большого количества полей в одном из наших списков, мне захотелось разнести эти поля по вкладкам. Причем, захотелось также следующее:
  1. Чтобы сохранилась возможность добавлять новые поля в список
  2. Чтобы можно было перемещать поля из одной вкладки в другую, и менять порядок их следования
  3. Чтобы поля рендерились стандартными средствами 
Для того, чтобы определить принадлежность полей к вкладкам, я раскопал способ добавления пользовательских данных в SPField, и создал симпатичный jquery-интерфейс для того, чтобы этим мог заниматься пользователь. Но дальше дело, неожиданно, застопорилось: оказалось, что народ повсеместно пользуется для подобных целей грязными js-хаками, а такой способ мне категорически не подошел!

И вот, после долгих исследований и поиска в интернетах, мне-таки удалось понять, как же это делается правильно, и воплотить в жизнь вот такую вот красивую форму:



Как видите, часть полей (ФИО и фотография) здесь рендерятся по-особому. Остальные поля рендерятся обычным образом, но разбиваются на вкладки (пользователь может менять порядок полей и принадлежность полей разным вкладкам).

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