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

Custom Workflow Action: Количество рабочих дней между датами

Многие недооценивают/недолюбливают SPD Workflows - это рабочие процессы, которые создаются в SharePoint Designer. Обычный аргумент заключается в том, что они недостаточно гибкие и много чего не позволяют, в то время как де Visual Studio Workflows - это C#, и там уж все можно. Ну-ну :)

На самом деле SharePoint Designer Workflows - очень даже гибкие. Во многом - благодаря возможности создания для них программных Custom Actions, причем эти Custom Actions можно создавать даже для Sandbox, и следовательно - использовать в O365. Ниже я покажу, как это сделать, на реальном примере.

Проблема вычисления количества рабочих дней между датами


Проблема это известная и не новая. С помощью вычисляемого поля (Calculated Field) можно легко вычислить количество календарных дней между датами, и после некоторых ковыряний даже можно исключить субботу и воскресенье - но как быть с праздниками!? Праздники нередко переносятся туда-сюда, т.е. каждый год решение о распределении праздников принимается нашим правительством, так что предугадать/рассчитать это распределение попросту невозможно: сегодня так решили, а завтра начинают январские праздники на май переносить и т.д. :)

В то же время, например для подсчета длины отпуска, или например Due Date для счета или другого документа - очень пригодилось бы уметь вычислять эту разницу!

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

Почему Custom Workflow Action?


Workflow заточены для построения бизнес-процессов. Именно в бизнес-процессах требуется знать разницу в рабочих днях чаще всего. Например, для рассылки напоминаний исполнителям "за 3 рабочих дня" до истечения срока рассмотрения заявки и т.п.

Как я уже упоминал в своем вводном посте про Workflow, рабочие процессы нужно использовать, когда требуется организовать бизнес-процесс, т.е. взаимодействие системы с людьми, особенно если участников вовлечено в процесс много. Если процесса никакого нет - лучше использовать Event Receiver'ы и Timer Job'ы. Повально использовать WF - идея дурная.

Workflow Action XML


Самая интересная часть написания Workflow Action - это создание т.н. "Workflow Action Statement", т.е. определение, как будет внешне выглядеть сконструированный вами шаг процесса.

Это делается полностью через XML. Настройка на удивление очень простая и гибкая. Все начинается с определение атрибута Sentence элемента RuleDesigner. Например, для нашего случая это может выглядеть так:

<RuleDesigner Sentence="Count holidays between %1 and %2 (result to %3)">

Дальше, как можно догадаться, нужно задать настройки для параметров, определенных через плейсхолдеры %1, %2 и %3. Это делается вложенными элементами FieldBind:

<RuleDesigner Sentence="Count holidays between %1 and %2 (result to %3)">
    <FieldBind Field="startDate"
               Text="start date" Id="1"
               DesignerType="Date" />
    <FieldBind Field="endDate"
               Text="end date" Id="2"
               DesignerType="Date" />
    <FieldBind Field="Result"
               Text="HolidaysCount" Id="3"
               DesignerType="ParameterNames" />
</RuleDesigner>

Как нетрудно догадаться, атрибут Id задает номер плейсхолдера, атрибут Text задает, какой текст будет отображаться вместо соответствующего плейсхолдера по умолчанию (когда еще не выбрано конкретное значение), а Field - это внутренний идентификатор поля, который потребуется немного позже. Таким образом, в SharePoint Designer заданный таким образом Custom Workflow Action будет выглядеть следующим образом:


По-моему, здорово! :)

Безусловно, самый интересный атрибут элемента FieldBind - это DesignerType. Например, значение "Date" для этого атрибута задает вот такой дизайнер для элемента:


Обратите внимание: рядом с полем ввода две кнопки! Одна ([...]) вызывает обозначенный на скриншоте диалог ввода "фиксированного значения" даты/времени. Другая же ([fx]) вызывает стандартный Lookup-диалог, который позволяет вставить значения поля, переменной, параметра и т.п.:



Существует, конечно же, множество других типов дизайнеров, которые можно использовать. Есть среди них дизайнеры для разных типов значений - Bool, Integer, Float, Hyperlink и др., есть преднастроенные выпадающие списки: выбор одного из полей текущего списка, выбор одного из списков текущего узла, и т.д., есть выпадающие списки для которых можно задать список значений, а есть даже возможность затянуть список значений из внешнего датасорса (мне правда еще ни разу такое не требовалось). В общем, вариантов масса.

Все комбинации можно посмотреть в файле 14\TEMPLATE\XML\WorkflowActions.xsd. Довольно доходчивое описание, что каждый из этих дизайнеров собой представляет (правда, без скриншотов), можно найти на MSDN.

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

На самом деле, возможно даже не все об этом знают, но любой элемент SPD Workflow имеет не только "визуальное" представление в виде конструктора, но также представление в виде PropertyGrid, которое вызывается через пункт контекстного меню "Properties":


В этом представлении может иногда оказаться больше свойств, чем полей в конструкторе шага. Эти свойства также задаются через XML, элементами Parameter, и упомянутый атрибут Field элемента FieldBind как раз должен совпадать с Parameter/Name. Для элементов Parameter задается C#-тип этого параметра, а также направление (In или Out) и некоторые другие свойства. Параметры уже непосредственно передаются в C# код, который реализует данный Workflow Action. Вот как выглядит XML, задающий параметры для нашего случая:

<Parameters>
  <Parameter Name="__Context"
              Type="Microsoft.SharePoint.WorkflowActions.WorkflowContext, Microsoft.SharePoint.WorkflowActions"
              Direction="In"
              DesignerType="Hide"/>
  <Parameter Name="startDate"
              Type="System.DateTime, mscorlib"
              Direction="In"
              DesignerType="Date"
              Description="Start date of the holidays period" />
  <Parameter Name="endDate"
              Type="System.DateTime, mscorlib"
              Direction="In"
              DesignerType="Date"
              Description="End date of the holidays period" />
  <Parameter Name="Result"
              Type="System.Int32, mscorlib"
              Direction="Out"
              DesignerType="ParameterNames"
              Description="Number of holidays between two dates." />
</Parameters>

Параметр __Context - служебный, в процессе исполнения он будет автоматически подменен объектом контекста Workflow.

Обратите внимание, здесь еще раз задается DesignerType. Этот атрибут определяет вид дизайнера в PropertyGrid. Он вполне может отличаться от DesignerType соответствующего элемента FieldBind.

Результирующий PropertyGrid с параметрами заданными приведенным выше XML будет выглядеть следующим образом:



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

Кроме параметров, еще необходимо задать общие настройки для создаваемого Workflow Action. За это отвечает элемент Action - который будет задавать название нашего Workflow Action, его тип, категорию и т.п. Вот как всё это будет выглядеть в итоге:

  <WorkflowActions>
    <Action Name="Count Holidays"
            SandboxedFunction="true"
            Assembly="$SharePoint.Project.AssemblyFullName$"
            ClassName="MyProject.CustomActions"
            FunctionName="CountHolidays"
            UsesCurrentItem="true"
            AppliesTo="all"
            Category="Utility Actions">
      <RuleDesigner Sentence="Count holidays between %1 and %2 (result to %3)">
        ...
      </RuleDesigner>
      <Parameters>
        ...
      </Parameters>
    </Action>
  </WorkflowActions>

В случае затруднений с XML, примеры определения имеющихся в SharePoint Workflow Actions в XML-виде можно найти в папке 14\TEMPLATE\1033\Workflow, в файлах *.actions.

Да, последнее про XML: этот XML деплоится через модуль, т.е. этот XML-код должен быть помещен в Elements.xml.

Метод-обработчик


Наконец, XML написан, и теперь можно переходить наконец-то на C#. Как не трудно догадаться, метод-обработчик задается в XML атрибутами Assembly, ClassName и FunctionName элемента Action.

Метод должен возвращать Hashtable, и принимать все параметры обозначенные в XML, строго в том порядке, в котором идут элементы Parameter. Типы, ясное дело, тоже должны соответствовать заявленным. А вот имя параметра можно сделать другим (но лучше тоже чтобы они совпадали, просто чтобы не запутаться потом).

Таким образом, в моем случае сигнатура метода должна выглядеть следующим образом:

public Hashtable CountHolidays(SPUserCodeWorkflowContext context, DateTime startDate, DateTime endDate)

Возвращать значения нужно в Hashtable, ключ значения должен соответствовать значению атрибута Parameter/Name в XML. Т.е. в нашем случае будем возвращать значение в hashtable["Result"].

Зная это, осталось только написать собственно код для подсчета количества выходных согласно заданному вами алгоритму, используя объект контекста и переданные параметры.

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

public Hashtable CountHolidays(SPUserCodeWorkflowContext context, DateTime startDate, DateTime endDate)
{

    using (SPSite site = new SPSite(context.CurrentWebUrl))
    {
        using (SPWeb web = site.OpenWeb())
        {
            try
            {
                var list = web.Lists[context.ListId];
                var item = list.GetItemById(context.ItemId);
                        
                var holidaysList = // здесь получайте список с праздниками
                var query = new SPQuery()
                {
                    ViewFields = @"<FieldRef Name=""ID"" />",
                    ViewFieldsOnly = true,
                    Query = String.Format(@"<Where>
                        <And>
                            <Geq>
                                <FieldRef Name=""{0}"" />
                                <Value IncludeTimeValue=""FALSE"" Type=""DateTime"">{1}</Value>
                            </Geq>
                            <Lt>
                                <FieldRef Name=""{0}"" />
                                <Value IncludeTimeValue=""FALSE"" Type=""DateTime"">{2}</Value>
                            </Lt>
                        </And>
                        </Where>", 
                        "HolidayDateFieldInternalName", 
                        SPUtility.CreateISO8601DateTimeFromSystemDateTime(startDate.Date), 
                        SPUtility.CreateISO8601DateTimeFromSystemDateTime(endDate.Date.AddDays(1)))
                };
                var items = holidaysList.GetItems(query);

                return new Hashtable() { { "Result", items.Count } };
            }
            catch (Exception ex)
            {
                SPWorkflow.CreateHistoryEvent(web, context.WorkflowInstanceId, 0, web.CurrentUser, TimeSpan.Zero, "Error", ex.Message + " Stack trace: " + ex.StackTrace, string.Empty);
                return new Hashtable() { { "Result", 0 } };
            }
        }
    }

}

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

Замечание: приведенные выше код и XML относятся к созданию Sandboxed Workflow Custom Action. Обычные серверные Custom Workflow Actions имеют свои преимущества - к примеру, они позволяют запускать код с повышенными привилегиями, что в Sandbox'е сделать не получится. Такие Workflow Actions работают немного по-другому и определение метода будет другим (хотя XML примерно такой же). Как создавать серверные Workflow Actions, описано например здесь.

Где еще взять Workflow Custom Actions


На codeplex можно найти десятки Custom Actions, уже созданных для вас. Вы можете использовать их как пример, дорабатывать, изменять и т.п. - ведь это Opensource! Например, spdactivities.codeplex.com.

Кроме того, есть компании, которые продают пакеты Custom Actions, и там попадаются очень даже интересные элементы. Например, HarePoint Workflow Extensions - сам не использовал, но на взгляд выглядит интересно.

Заключение


SPD Workflow - на самом деле очень даже гибкая штука, благодаря возможности создания собственных Workflow Actions. В SharePoint 2013, как известно, Visual Studio Workflows уже работают вообще только в режиме совместимости...

пятница, 26 апреля 2013 г.

Про эффективность и продуктивность

Делать быстро и делать именно то, что надо - это идеал, который на самом деле никому не нужен. Да-да, я не ошибся! Реальность - грустная штука и иногда нелогичная, но от нее никуда не убежать :(

Моя история


Примерно два с половиной года назад я пришел к следующему выводу: у меня достаточно знаний и опыта, я хорошо схватываю, я прекрасно решаю задачи, я даже в некотором роде люблю SharePoint :), но.... Но очень рассеян, часто отвлекаюсь. Не хватает концентрации, не хватает самодисциплины. В результате проваливаю сроки, целыми днями страдаю фигней и т.п. Не хватает, вроде как, совсем немногого, чтобы быть "очень классным" :) К такому заключению я пришел, повторюсь, около 2.5 лет назад, и решил, что надо это менять.

Итак, я стал работать над тем, чтобы стать лучше. Выяснилось, чтобы этого добиться, нужно "прокачать" два основных скилла:
  1. Продуктивность - это умение работать максимально быстро, но без потери качества.
  2. Результативность - это умение работать на результат, т.е. делать правильные вещи, чтобы достигнуть цели.
Почувствуйте тонкую разницу! Т.е. нужно не только уметь работать быстро, нужно еще уметь делать правильные вещи. Если вы быстро сделаете какую-то задачу, но поймете ее неправильно - какова цена вашим трудам? Если вы классно сделаете какой-нибудь функционал, но потом окажется, что клиенту он не очень то был и нужен - как вы будете себя чувствовать потом? :)

Итак, что я делал, чтобы этого добиться:
  1. Установил RescueTime (в минимальном, бесплатном варианте). Очень классная утилита, которая позволяет отследить, сколько ты работаешь в том или ином приложении. Приложения распределены по категориям, и каждая категория оценивается от -2 до +2 (это все настраивается). Фишка состоит в том, что через месяц-два тебе надоедает постоянно следить за своими отчетами, НО умный RescueTime присылает еженедельные отчеты, и сразу видно, если "расслабился" и надо "подтянуть" концентрацию.
  2. Читал статьи, смотрел доклады по этой теме - особенно классный доклад от Хансельмана.
  3. Стал планировать свои дела - кстати не только рабочие, но и персональные (такие в частности, как отъезд в Финляндию и все необходимое для этого).
  4. Изучал Usability и UX, чтобы понимать, что действительно удобно для пользователей, а что нет. Как следствие, я начал задавать более правильные вопросы при анализе задач, и соответственно, стал работать с большей результативностью и с большей пользой для всех.
На самом деле, много всего читал, много всяких подходов пробовал. Что-то давало результат, что-то нет. В конечном итоге, мне удалось значительно продвинуться в обоих умениях, и как минимум субъективно, я чувствую что стал работать гораздо быстрее (очень сильно быстрее, примерно в 2-3 раза) и уделять больше внимания действительно важным вещам.

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

Почему быть слишком продуктивным - вредно?


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

Простой пример: вот представьте, что есть очень классный спец, который восстанавливает данные с умерших жестких дисков. Вы приходите к нему и просите восстановить данные. Он говорит: 10 тысяч рублей. Вы спрашиваете: а когда будет готово? Он говорит: это сложная процедура, займет 2 недели. Вы говорите ок, отлично (про себя думая: ну нифига себе, это ему две недели работать, за 10 тыс. рублей, ну да, тут никуда не денешься, придется платить).

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

И здесь вы начинаете чувствовать себя некомфортно. "Нифига себе молодец," - думаете вы, - "10 тысяч за полчаса. Это кто ж такие деньги зарабатывает!".

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

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

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

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

(Если кому-то интересно узнать, почему так происходит - рекомендую курс Dan Arley про иррациональное поведение на Coursera)

Почему работать целый день без остановки - нельзя в принципе?


По многим причинам:
  1. Во-первых, мозгам нужен отдых и время на смену "контекста" между задачами.
  2. Во-вторых, для решения многих задач вообще говоря нужно время, чтобы они "поварились" у вас в голове. Это из серии "утро вечера мудренее".
  3. В-третьих, если вам все-таки удастся работать постоянно и без передышки, у вас просто будут скорее всего очень быстро заканчиваться задачи и возникнут т.н. "периоды ожидания", которые вас будут расслаблять, а ваше начальство будет беситься, потому что им придется платить за то что вы ничего не делаете и ждете, а кто это любит?...
На практике это означает, что нельзя сидеть за компьютером 8 часов в день и писать код. Это неправильно и неэффективно. Т.е. рано или поздно вы себя обнаружите гуляющим по офису и "ничего не делающим" с точки зрения начальства. Это неизбежно.

Если соединить эти рассуждения с тем, о чем я рассказывал в предыдущем параграфе, получаем, что эффективным и продуктивным людям НИКТО не хочет платить деньги. Это факт :)

Кстати отсюда же напрямую исходит, что невозможно сократить часы своей работы официальными методами. Например, я хороший специалист, я хочу работать 4 часа в день вместо 8, и больше тратить на семью. Я уверен, что я буду делать не меньше по объему за 4 часа, чем мой сосед "Вася" за 8 - и хочу получать за 4 часа такую же зарплату, как он за восемь. НЕ ПОЛУЧИТСЯ! Точнее, делать столько же получится, а получать за 4 часа такую же зарплату - нет. Причем, на вдвое большую зарплату за 8 часов договориться еще реально, но сокращение рабочего дня - нет. Психология, блин.

Как решить эту проблему?


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

Но мне кажется, можно кое-что придумать и работая "на дядьку":
  1. Надо поменьше показываться начальству/клиентам на глаза, особенно во время работы.
  2. Надо добиваться положительного фидбэка от клиентов/рецензоров. Выполнять работу хорошо, в сроки, и всегда демонстрировать результат.
  3. Во время демонстраций, не помешает рассказать о тех трудностях, которые пришлось преодолеть в процессе выполнения этой задачи. Также не помешает упомянуть о том, какие знания и опыт вам помогли в решении этой задачи.
  4. Желательно выбирать такую работу, в которой не придется заносить часы и не придется отчитываться по каждому часу.
  5. Свободное время проводить там, где тебя никто не видит, и особо об этом не упоминать.
Если вкратце, работать нужно так, чтобы никто не видел процесса, а все видели только результат.

Заключение


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

P.S. Не стесняйтесь свои мысли/опыт по этому поводу скидывать в комменты. Обсудим :)

вторник, 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 помогут сделать ваши представления лучше. Удачи!