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

12 комментариев:

  1. Здравствуйте, Андрей!

    Подскажите, пожалуйста, а способ реализации метода-обработчика, который возвращает Hashtable -- это какая-то новинка в шарепоинте?

    Когда я писал свои Custom Actions, я делал целый класс с набором dependency property и реализацией метода Execute. И оно тоже работало. Получается, это два разных подхода?

    ОтветитьУдалить
    Ответы
    1. Добрый день!

      Цитата из статьи:

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

      Серверные (Full trusted) Workflow Actions - это как раз с методом Execute. В том же абзаце есть ссылка на пример статьи, которая освещает этот способ.

      Удалить
  2. Андрей, привет.

    "Повально использовать WF - идея дурная."

    У меня есть идея организации SPD Workflows, хотел спросить насколько она дурная.

    Проблема:
    В 2010 spd wf последовательные, т.е. нельзя использовать stages как в sp2013.

    Есть событие WorkflowCompleted. Я хочу на каждом этапе рабочего процесса менять поле статуса элемента списка и завершать РП. После этого срабатывает ресивер WorkflowCompleted и если все ок, запускает РП заново.

    Таким образом можно симулировать state machine wf и переводить заявку на любой этап сколько угодно раз.

    Конкретно в моем РП, количество перезапусков будет примерно 2-3 на каждую заявку.


    Использовать кастомный wf с кодом очень не хочется, а добиться такого поведения через Custom Workflow Action видимо нельзя. Схема, когда один РП пинает другой РП мне тоже кажется не очень верной.

    Насколько дурна идея? Из минусов пока вижу что wf history на каждый инстанс будет своя.

    ОтветитьУдалить
    Ответы
    1. > Повально использовать WF - идея дурная
      Я имел в виду, что например для какой-то технической обработки списков, документов и т.п., желательно использовать Timer Job'ы и Event Receiver'ы. Для бизнес-процессов - WF. Например, если есть циклический бизнес-процесс, то нет ничего плохого в том, чтобы его реализовать через циклический рабочий процесс. Также и со state machine.

      На самом деле, обычно, рабочие процессы уже циклические и если подумать они уже state machine. Т.е. например в стандартном Approval Workflow, когда создается задача на одобрение, можно бессчетное число раз перепинывать задачи туда-сюда, требовать изменения документа и т.п. Это тоже собственно изменение статусов и циклы, и в идеале так и надо делать.

      Если уж совсем нужен прямой цикл, в SPD 2010 Workflow это реализуется через обновление элемента списка, к которому привязана Workflow. Это обновление нужно делать после хотя бы небольшой задержки, чтобы WF переехал в OWSTimer. Без всяких ресиверов.

      Кстати есть еще один неплохой вариант - разрешить LoopActivity, но к сожалению для этого придется править web.config.

      Удалить
  3. К минусам: накладные расходы на инстансы, задержки между переходами заявки по этапам.

    ОтветитьУдалить
  4. "Повально использовать WF - идея дурная." - вот мы как раз так и делаем :)
    плюсы: гибкость - если решение или его кусочек многократное, то под каждого клиента не заточишь, а так на месте взял и поменял тексты сообщений или набор конечных действий и т.п.
    середина: так то для запуска WF (при создании или изменении) тот же ресивер подвешивается, так то идеологически это тоже самое.
    минусы: накладные расходы ,запуск проца может повиснуть минуть на пять и т.п. косяков хватает, поэтому надо применять там где +/- 30 минут никакой роли не играют, а сбой не приведет к ...

    из опыта:
    1) на практике получается, что надо порядка пяти таких ресиверов, которые будут мониторить статусы, что то делать(например аудит смены овнера) и рассылать уведомления и т.п.
    2) из сторонних прилижись только ILoveSharepoint, там есть такие как - работы с SQL, WebServices, Regexp, PowerShell - вот эта самая активно использующаяся активитя, в сандбокс конечно уже не прокатить.
    и бубунец с LoopActivity.
    3) И написали (или пишем), если не хватает, свои. из самых самых: 1)оправка почты с возможностью, вложений из разных источников, HTML со выстроенными картинками, указать свой набор Email HEADERS, From 2) запуск рабочих процессов на текущем и на других (через CAML) элементах с возможностью передачи параметров.

    а так стараемся использовать штатные возможности по максимуму, правда жись такая, что в сложных процах без PS тяжаловато.

    ОтветитьУдалить
    Ответы
    1. Александр бубинец с LookActivity вот этот? - http://vojtan.wordpress.com/2010/10/07/creating-loops-in-sharepoint-designer-2010-workflows/


      Как я понял вы создаете wf в spd. Как вы их потом развертываете клиентам? Или на месте все делается и деплой не нужен.

      Удалить
    2. 1) LookActivity - да именно этот.
      2) да делали в SPD, переносится без проблем, в инете найдете кучу материалов, например http://social.technet.microsoft.com/forums/en-US/sharepointcustomizationprevious/thread/9eb11f47-115b-4f21-8bbf-37b327f5426a/ (позже, с работы и если не забуду, дам ссылку по которой мы делали)

      из опыта: если у вас есть узел который содержит WF, то сохранением узла как шаблона (wsp) и его последующего деплоя на другом сервер, все работает почти отлично, и формы IP и WF. тоже самое касается списков.
      по сути что такое WF сделанное в SPD? да просто набор xml файлов - вот их перенесли на другой сервер и все.

      вообще у нас последняя тенденция:
      1) клацаем все мышкой - поля кт формы вф и т.п.
      2) сохраняем как wsp и над ним шаманим, по сути удаляем нахрен все упоминания про sitecollection features из xml определения шаблона узла, что бы активироваться в новом месте не мешали.
      3) деполоим на другом серваке, при этом соблюдаем что бы все сторонние необходимые wsp и фичи были установлены и активированы - ресиверы, ilove activites, активированы все нужные фичи и т.п.

      Удалить
    3. вот инструкция по которой делаем мы: http://platinumdogs.me/2013/02/05/sharepoint-deploy-a-declarative-globally-reusable-workflow-with-a-feature-using-visual-studio/

      Удалить
    4. Спасибо.
      Хак с LoopActivity очень хрупкий, как я понял. При каждом изменении WF через SPD - loop исчезает =( Для боевого решения не подходит.

      Удалить
  5. Вместо лупов лучше найти набор активностей, где есть "Start Workflow" и перезапускать самих себя. Отлично работает, если конечно не надо 10 разных лупов в воркфло делать ;))

    Либо можно взять шедалер (бесплатный HarePoint Workflow Scheduler для 2007-2013) и запускать воркфло по расписанию -- если природа задачи такое позволяет.

    ...а в O365 так все зарезано, что большинство практических задач реализуются с нечеловеческими вывертами :(

    ОтветитьУдалить
  6. Коллеги добрый день подскажите как правильно разворачивать Workflow Custom Declarative Activity?

    собрал wsp развернул его на сайте.

    оно даже появилось в доступных действиях.

    но не подхватил параметры, параметры в тексте выглядят как %1 и %2

    пытаюсь вот по этому примеру сделать.

    http://raquelalineblog.wordpress.com/2013/05/11/sharepoint-2013-workflow-custom-declarative-activity/

    ОтветитьУдалить

Внимание! Реклама и прочий спам будут беспощадно удаляться.

Примечание. Отправлять комментарии могут только участники этого блога.