четверг, 13 июня 2013 г.

Рабочие процессы SharePoint 2013: Workflow Services API в примерах

В предыдущем посте я очень кратко (но ёмко) прошелся по Workflow в SharePoint 2013. Сегодня хочу продолжить тему и рассказать о новом клиентском API под названием Workflow Services (и показать, как его можно использовать), которое доступно в том числе из SharePoint JavaScript Object Model.



Зачем оно нужно?


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

По большому счету, теперь можно создать полномасштабный редактор/менеджер рабочих процессов полностью на JavaScript! В более мелком масштабе, это позволяет например делать следующие чрезвычайно полезные вещи:
  1. Массово запускать/останавливать/ставить на паузу/возобновлять рабочие процессы.
  2. Выполнять массовое изменение/обработку рабочих процессов (!). Скажем, у вас есть несколько разных рабочих процессов с какой-то захардкоженной общей частью, и потом вдруг бизнес-процесс изменяется и вам нужно изменить общую часть во всех этих рабочих процессах. Пишете приложение на CSOM или скрипт на JSOM - и вуаля, задача решена!
  3. Выполнять более эффективное автоматизированное тестирование Workflow.
  4. Создать "генератор" рабочих процессов по шаблону. Это пригождается на самом деле нередко, особенно с учетом того, что Reusable Workflow нового типа нельзя публиковать глобально на коллекцию узлов, как это можно делать со старыми РП - т.е. если хочется создать какой-то единый рабочий процесс на коллекцию сайтов - привет, копипаст!

Описание API


Для JavaScript Object Model, API находится в файле /_layouts/15/SP.WorkflowServices.js. В процессе создания проекта TypeScript Definitions for SharePoint 2013 мне пришлось полностью это API исследовать и описать, так что подробные комментированные спецификации доступны для изучения.

Вкратце, что там есть:
  1. WorkflowDeploymentService - этот сервис собственно позволяет считывать определения рабочих процессов, изменять их, создавать новые, удалять и т.д. Отсюда же можно управлять видимостью рабочих процессов: например опубликовать рабочий процесс или же пометить некий рабочий процесс как "deprecated". Наконец, через этот сервис мы можем получить текущие настройки сервера (список WF actions), экспортировать какой-нибудь из рабочих процессов в wsp файл и сделать некоторые другие полезные вещи.
  2. WorkflowSubscriptionService - этот сервис позволяет прицеплять рабочие процессы к спискам и отцеплять их.
  3. WorkflowInstanceService - этот сервис позволяет управлять уже непосредственно экземплярами рабочих процессов - стартовать РП, ставить на паузу и возобновлять, отменять или принудительно завершать (terminate). Также именно через этот сервис можно получить список выполняющихся в данный момент рабочих процессов.
  4. InteropService - этот сервис служит для совместимости и позволяет запускать рабочие процессы SharePoint 2010 из SP2013. Как это работает - подробно описано на MSDN.

Пример 1: получаем список доступных WF actions

Этот пример я создавал как раз в рамках проекта TypeScript Definitions for SharePoint 2013. По большому счету, нам нужно вызвать метод WorkflowDeploymentService.getWorkflowActivities - и отобразить всё что он вернет. Проще некуда? ;)

Код:

SP.SOD.executeOrDelayUntilScriptLoaded(function () {
    var context = SP.ClientContext.get_current();
    var web = context.get_web();
    context.load(web);

    context.executeQueryAsync(function (sender, args) {

        var servicesManager = SP.WorkflowServices.WorkflowServicesManager.newObject(context, web);
        context.load(servicesManager);

        context.executeQueryAsync(function (sender, args) {

            var deploymentService = servicesManager.getWorkflowDeploymentService();
            context.load(deploymentService);

            context.executeQueryAsync(function (sender, args) {

                var designerActions = deploymentService.getDesignerActions(web);

                context.executeQueryAsync(function (sender, args) {

                    $get('results').innerHTML = STSHtmlEncode(designerActions.get_value());

                }, function (sender, args) {
                    alert("Error operating with services! " + args.get_message());
                });
            }, function (sender, args) {
                alert("Error loading services! " + args.get_message());
            });
        }, function (sender, args) {
            alert("Error loading serviceManager! " + args.get_message());
        });
    }, function (sender, args) {
        alert("Error loading web! " + args.get_message());
    });
}, "sp.workflowservices.js");


Забавно, не правда ли? Чтобы сделать даже простейшее действие с Workflow Services API, пришлось сделать аж 4 запроса к клиентской объектной модели!

Скриншоты (из проекта примеров SPTypeScript):

Результат:



Пример 2: получаем список рабочих процессов в коллекции сайтов

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

Готовый код:

(function() {
    
 var resultsElement = $get('results');
 var context = SP.ClientContext.get_current();
 var rootWeb = context.get_site().get_rootWeb();                        
 var webs = context.get_web().get_webs();                        
 context.load(rootWeb);
 context.load(webs);
 context.executeQueryAsync(function (sender, args) {
     
     getWorkflowDefinitions(context, rootWeb, resultsElement);
  
     var websEnum = webs.getEnumerator();
 
     while (websEnum.moveNext())
     {
             var web = websEnum.get_current();
      getWorkflowDefinitions(context, web);
     }

 }, errFunc);

 function errFunc(sender,args) {
     alert("Error occured! " + args.get_message());
 };
 

 
 function getWorkflowDefinitions(context, web)
 {
  var servicesManager = SP.WorkflowServices.WorkflowServicesManager.newObject(context, web);
  context.load(servicesManager);

  context.executeQueryAsync(function (sender, args) {

   var deploymentService = servicesManager.getWorkflowDeploymentService();
   context.load(deploymentService);

   context.executeQueryAsync(function (sender, args) {

    var workflowDefinitions = deploymentService.enumerateDefinitions(false);
    context.load(workflowDefinitions);
    context.executeQueryAsync(function (sender, args) {
     var definitionsEnum = workflowDefinitions.getEnumerator();
 
     var empty = true;
     
     if (resultsElement.innerHTML.indexOf('Loading') !== -1)
       resultsElement.innerHTML = '';

     resultsElement.appendChild(document.createTextNode('Site ' + web.get_url() + ':'));
     resultsElement.appendChild(document.createElement('br'));
 
     while (definitionsEnum.moveNext()) 
     {
      var def = definitionsEnum.get_current();
      resultsElement.appendChild(document.createTextNode(
       def.get_displayName() + " (id: " + def.get_id() + ")"
      ));
      resultsElement.appendChild(document.createElement('br'));
      empty = false;
     }
     
     if (empty)
     {
      resultsElement.appendChild(document.createTextNode('No 2013 workflows found.'));
      resultsElement.appendChild(document.createElement('br'));
     }
     
    }, errFunc);
   }, errFunc);
  }, errFunc);
 }    


})();


Опять же ничего особенного здесь нет, но громоздко :(

Обратите внимание: рабочие процессы режима SharePoint 2010 этим кодом отображены не будут! API прежде всего предназначено для работы с новыми WF.

Результат исполнения скрипта:


Полный файл с этим примером доступен для скачивания. Просто закиньте этот файл в любую библиотеку документов или напрямую в SPD - и зайдите из браузера.

Пример 3: копирование рабочих процессов между сайтами

Я уже упоминал, что рабочие процессы нового типа нельзя публиковать на всю коллекцию сайтов. В случае общих рабочих процессов для узлов проектов или рабочих групп, копипаст РП неизбежен. Благодаря WF Services API, его хотя бы можно автоматизировать и ускорить - и на том спасибо...

Для реализации понадобятся методы getDefinition и saveDefinition всё того же сервиса WorkflowDeploymentService. При необходимости, при копировании определение РП можно кастомизировать, затачивая под каждый конкретный сайт.

(function() {

 var resultsElement = $get('results');
 var myDefinitionId = 'PUT-YOUR-GUID-HERE';
 var context = SP.ClientContext.get_current();
 var rootWeb = context.get_site().get_rootWeb();                        
 var webs = rootWeb.get_webs();                        
 context.load(rootWeb);
 context.load(webs);
 context.executeQueryAsync(function (sender, args) {
     
  var servicesManager = SP.WorkflowServices.WorkflowServicesManager.newObject(context, rootWeb);
  context.load(servicesManager);
  context.executeQueryAsync(function (sender, args) {
   var deploymentService = servicesManager.getWorkflowDeploymentService();
   context.load(deploymentService);
   context.executeQueryAsync(function (sender, args) {
    var workflowDefinition = deploymentService.getDefinition(myDefinitionId);
    context.load(workflowDefinition);
    context.executeQueryAsync(function (sender, args) {
     
        var websEnum = webs.getEnumerator();
    
        while (websEnum.moveNext())
     {
                var subweb = websEnum.get_current();
         saveWorkflowDefinition(context, subweb, workflowDefinition);
     }
     
    }, errFunc);
   }, errFunc);
  }, errFunc);

 }, errFunc);

 function errFunc(sender,args) {
     alert("Error occured! " + args.get_message());
 };
 

    function saveWorkflowDefinition(context, web, myDefinition)
    {
  var servicesManager = SP.WorkflowServices.WorkflowServicesManager.newObject(context, web);
  context.executeQueryAsync(function (sender, args) {
   var deploymentService = servicesManager.getWorkflowDeploymentService();
   context.load(deploymentService);
   context.executeQueryAsync(function (sender, args) {

           myDefinition.set_displayName(myDefinition.get_displayName());

    var defId = deploymentService.saveDefinition(myDefinition);
    deploymentService.publishDefinition(myDefinition.get_id());
    context.executeQueryAsync(function (sender, args) {
    
     if (resultsElement.innerHTML.indexOf('Loading') !== -1)
       resultsElement.innerHTML = '';

     resultsElement.appendChild(document.createTextNode('Definition saved to site ' + web.get_url() + ', ID: ' + defId.get_value()));
     resultsElement.appendChild(document.createElement('br'));
     
    }, errFunc);
   }, errFunc);
  }, errFunc);
    }

})();


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

Всё остальное вроде банально.

Результат работы:

После этого РП появился на дочернем сайте:


И стал доступен для привязки к спискам:

Я даже его привязал к списку и запустил, работает :)

Заключение


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

Удачи!


Update: Улучшил примеры, добавил несколько интересных дополнительных примеров, оформил и опубликовал на CodeProject: http://www.codeproject.com/Articles/607127/Using-SharePoint-2013-Workflow-Services-JS-API

4 комментария:

  1. WorkflowSubscriptionService отвратно назвали, потому как есть уже такой класс в workflow runtime, плюс теряется привычная разработчикам семантика слова ассоциация. Да и дизайн его как то через жопу сделан. В WorkflowSubscription указание списка задач происходит через вызов SetProperty("TaskListId",taskListId), что мерзко выглядит, лишает нас интеллисенса, да еще список этих свойств не отражен в документации.

    ОтветитьУдалить
    Ответы
    1. Хорошо пропитанный ненавистью коммент :)

      WorkflowSubscriptionService и вправду кривой. И класс WorkflowSubscription - тоже. Я хотел включить примеры по нему в этот пост, но не смог вовремя разобраться как это всё использовать :)

      Удалить
  2. Great post, i'm particularly looking for publishing Workflow Activity for Office 365 Sites, how do we do it

    ОтветитьУдалить
  3. На странице Wrkstat.asx правлю javascript, хочу чтоб при нажатие на ссылку 'Завершить рабочий процесс.' состояние рабочего процесса менялось на 'Отменен'. По умолчанию почему-то так не сделано!?
    Но в скрипте instance.set_userStatus('Отменен') почему-то не отрабатывает

    function TerminateWorkflow4(instanceName)
    {
    showInProgressDialog();

    var ctx = SP.ClientContext.get_current();
    var wfManager = SP.WorkflowServices.WorkflowServicesManager.newObject(ctx, ctx.get_web());
    ctx.load(wfManager);
    ctx.executeQueryAsync (
    function(sender, args) {
    var instanceService = wfManager.getWorkflowInstanceService();
    var instance = instanceService.getInstance(instanceName);
    ctx.load(instance, 'UserStatus');
    ctx.executeQueryAsync (
    function(sender, args) {
    alert(instance.get_userStatus());
    instance.set_userStatus('Отменен')

    ctx.executeQueryAsync(
    function(sender, args) {
    }, errFunc
    );

    instanceService.terminateWorkflow(instance);
    ctx.executeQueryAsync(
    function(sender, args) {
    closeInProgressDialog();
    theForm.submit();
    }, errFunc
    );
    closeInProgressDialog();
    }, errFunc
    );
    }, errFunc);
    }

    function errFunc(sender,args) {
    closeInProgressDialog();
    alert("Error occured! " + args.get_message());
    };

    function closeInProgressDialog() {
    if(dlg != null)
    {
    dlg.close();
    }
    }
    function showInProgressDialog() {
    if(dlg == null)
    {
    dlg = SP.UI.ModalDialog.showWaitScreenWithNoClose("", "", null, null);
    }
    }

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

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