На самом деле 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 уже работают вообще только в режиме совместимости...