Перейти к содержанию

Разработка бизнес-процессов

Создание бизнес-процесса посредством Workflow API

Workflow API позволяет быстро и гибко создавать бизнес-процессы (БП), описывая их задания и переходы между ними, а также выделяя подпроцессы. Workflow API работает как специальное серверное расширение CardStoreExtension.BeforeCommitTransaction, при изменении карточки перед завершением транзакции. При этом активна транзакция на изменение сохраняемой карточки, поэтому при возникновении любых проблем, связанных с невозможностью выполнить какое-либо действие, произойдёт откат транзакции SQL и всех связанных изменений.

Рассмотрим основные понятия:

  • Задание – это те задания карточки, которые используются для формирования БП. Список типов заданий, поддерживаемых БП, заранее задаётся в коде расширения. У заданий есть один или несколько вариантов завершения, при выборе которых выполняется переход (см. ниже). Также при создании задания указываются параметры (произвольные данные, сериализуемые в BSON)

  • Переход – это действие, выполняемое в БП при старте или завершении подпроцесса, завершении задания или из другого перехода. Переход идентифицируется номером, уникальным в пределах подпроцесса. Переход может создавать задания, стартовать и завершать подпроцессы, вызывать другие переходы, инициализировать и ожидать счётчики, а также выполнять любые действия, такие как чтение и изменение данных карточки. При создании заданий переход может указывать параметры задания, а при создании подпроцесса – его параметры. Это позволяет указать номера переходов, выполняемые при завершении задания или подпроцесса.

  • Подпроцесс – это множество переходов, объединённое общей целью и имеющее список параметров (произвольных данных, сериализуемых в BSON). Подпроцесс идентифицируется уникальным идентификатором и текстовой строкой типа подпроцесса. Примером подпроцесса может служить параллельное (или последовательное) согласование по заданному списку ролей. Параметры подпроцесса могут изменяться в процессе выполнения его переходов (например, если в них содержится список ролей для последовательного согласования, то после завершения задания на каждую из ролей эта роль удаляется из списка и параметры пересохраняются). Параметры также могут содержать номера переходов родительского подпроцесса, которые будут выполнены по завершении этого подпроцесса. Подпроцесс имеет методы старта StartProcess и завершения StopProcess, задача которых – вызвать переходы.

  • Счётчик – это целое число, которое инициализируется некоторым положительным значением, и далее значение которого уменьшается вызовом метода DecrementCounter. Если значение стало равным нулю или меньше, то счётчик удаляется и код, вызвавший метод DecrementCounter, может выполнить некоторое действие. Посредством счётчика удобно реализовывать в БП блок “И”, который выполняет следующее за ним действие (переход) только в том случае, если было завершено определённое количество заданий в параллельном согласовании. Счётчик идентифицируется номером, уникальным для подпроцесса.

Рассмотрим следующий бизнес-процесс. Он состоит из подпроцесса 1 и глобального подпроцесса (т.к. весь процесс представляется в то же время как подпроцесс). В процессе используются задания двух типов.

Note

Посмотреть данный бизнес-процесс в работе, вы можете в Tessa Client, создав (требуются права администратора) карточку типа “Автомобиль” и нажав на левой панели карточки кнопку “Тестовое согласование”. Исходный код процесса поставляется вместе со всеми остальными расширениями типового решения (Extensions\Tessa.Extensions.Default.Client\Tiles\TestProcessTileExtension.cs и _Extensions\Tessa.Extensions.Default.Server\Workflow\TestProcess_).

  • Тип1: задание с двумя вариантами завершения “Вариант А” и “Вариант Б”. Это задание будет использоваться в разных частях процесса — как часть подпроцесса 1 согласования, а также как отдельное задание, причем в разных местах действия, выполняемые по вариантам завершения, будут разными.

  • Тип2: задание с одним вариантом завершения “Вариант В\Принять”.

Подпроцесс 1 отправляет три задания типа 1 параллельно. По завершении каждого из заданий он выполняет разные действия для каждого варианта завершения. После завершения всех заданий также выполняются какие-то действия, затем происходит выход из подпроцесса.

На схеме процесса цифрами (иногда в скобках) указаны номера переходов. Все переходы внутри подпроцесса локальны и их номера указаны с префиксом ЛП. В схеме используются два счетчика И с номерами Счетчик1 и Счетчик2. Номера счетчиков должны быть уникальны только в пределах своего подпроцесса. В схеме номера приведены разные для удобства объяснения.

Дальше опишем переходы и обработка завершения заданий.

Обработка завершения заданий (реализуется в расширении):

  • Тип1: при завершении из параметров задания определяются номера переходов, указанные при отправке задания и выполняется переход, соответствующий вариантам А или Б.

  • Тип2: при завершении берется из параметров задания и выполняется переход.

Локальные переходы (далее ЛП) внутри подпроцесса 1:

  • Старт процесса: инициализирует новый счетчик Счетчик* нулем и трижды запускает локальный переход 1.

  • ЛП 1: Запускает задание Тип1, настраивая его так, чтобы при завершении по варианту А — Выполнится ЛП 2, а при завершении по варианту Б — выполнится ЛП 3. Также увеличивает Счетчик1 на 1.

  • ЛП2: выполняет некоторые действия (завершилось задание по варианту А) и выполняет ЛП 4.

  • ЛП3: выполняет некоторые действия (завершилось задание по варианту Б) и выполняет ЛП 4.

  • ЛП4: уменьшает локальный счетчик Счетчик2, и, если он = 0, запускает ЛП 5.

  • ЛП5: что-то делаем (обрабатываем результат подпроцесса) и выполняем указанный в настройках глобальный переход. В варианте как на схеме это будет глобальный переход 4.

Глобальные переходы:

  • Старт процесса: инициализирует новый глобальный счетчик И числом 2 и выполняет переходы номер 1 и 2.

  • 1: Отправляет задание типа 1, настраивая его так, чтобы при завершении по варианту А выполнился переход 3, а по варианту Б - 4.

  • 2: Запускает подпроцесс 1, настраивая его так, чтобы при завершении выполнился переход 4.

  • 3: Отправляет задание Тип2, настраивая его так, чтобы при завершении выполнился глобальный переход 4.

  • 4: Уменьшает счетчик Счетчик1, если он = 0, выполняет переход 5.

  • 5: Запускает подпроцесс 1, настраивая его так, чтобы по завершении выполнился переход 6.

  • 6: Процесс завершен.

Реализация процесса состоит из следующих шагов:

  1. Реализовать IWorkflowManager или использовать стандартный класс WorkflowManager. Для обеспечения доступа к карточке-сателлиту при наличии таковой рекомендуется унаследоваться от WorkflowManager и добавить туда свойство Satellite, содержащее загруженную карточку-сателлит. Этот класс:

    • Обеспечивает доступ к контексту бизнес-процесса, содержащему:

      • запрос на сохранение карточки Request;

      • следующий запрос на сохранение NextRequest, в котором будут при необходимости добавлены задания;

      • результат валидации ValidationResult;

      • метаинформация по карточкам CardMetadata и др.,

    • Предоставляет средства к чтению и изменению информации по подпроцессу (включая параметры), по активным заданиям (включая параметры) и по счётчикам (включая декремент счётчика и проверку на равенство нулю).

  2. Реализовать IWorkflowWorker или унаследоваться от стандартного WorkflowTaskWorker<TManager> (предоставляющий стандартные возможности, где вместо TManager – реализация из предыдущего шага. Через свойство Manager можно получить доступ к IWorkflowManager’у, у которого запросить информацию по карточке, создать задание и др. При наследовании следует переопределить методы:

    • StartProcessCore – выполняет действия при запуске подпроцесса. Т.к. весь процесс также является подпроцессом, то здесь можно выполнить подготовительные действия при запуске процесса, например, изменить состояние карточки на “На согласование”.

    • StopProcessCore – выполняет действия при завершении подпроцесса, в т.ч. при завершении всего процесса. В параметрах подпроцесса могут быть получены переходы родительского подпроцесса, которые следует выполнить при завершении текущего подпроцесса.

    • CompleteTaskCore – выполняет действия при завершении задания. По типу задания, варианту завершения и параметрам задания в процессе можно определить выполняемые действия. Например, для каждого из вариантов завершения “Принять” и “Отказать” в параметрах задания могут быть указаны номера переходов в текущем подпроцессе, которые требуется выполнить.

    • RenderStepCore – выполняет переход с заданным номером для указанного подпроцесса. Например, переход может инициализировать счётчик через свойство Manager и выслать несколько заданий для параллельного согласования.

  3. Реализовать расширение на сохранение карточки, унаследовав его от WorkflowStoreExtension (если бизнес-процесс может применяться к карточками, добавленным в типовое решение, то рекомендуется унаследовать расширение от KrWorkflowStoreExtension). Это расширение связывает сохранение карточки с работой бизнес-процесса посредством реализации IWorkflowWorker, созданной на предыдущем шаге. Расширение определяет методы:

    • TaskIsAllowed – метод, определяющий, принадлежит ли это задание к бизнес-процессу и может ли его обработать метод IWorkflowWorker.CompleteTask. Принадлежность обычно определяют по идентификатору или имени типа задания.

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

    • StartProcess – метод, выполняющий запуск процесса согласования для заданной реализации IWorkflowWorker.

    • CreateWorker – создаёт реализацию IWorkflowWorker по заданной реализации IWorkflowManager.

  4. Реализовать расширения плиток TileExtension для запуска процесса, отзыва процесса и др.

Для реализации бизнес-процесса, описанного выше, можно обойтись без карточки-сателлита, и добавить в карточку платформенные коллекционные секции WorkflowCounters, WorkflowProcesses и WorkflowTasks, используемые стандартной реализацией WorkflowManager’а для хранения информации по счётчикам, подпроцессам и заданиям. Эти секции добавляются в карточку-сателлит в более сложных случаях, когда в ходе согласования требуется хранить дополнительные поля, такие как комментарий инициатора, состояние процесса и др., причём такие поля не должны быть частью карточки для того, чтобы после завершения задания не изменялась версия карточки (для обеспечения параллельности согласования).

Поскольку сателлита и дополнительных данных нет, используем стандартную реализацию IWorkflowManager – класс WorkflowManager, и на его основе реализуем IWorkflowWorker – наследник WorkfowTaskWorker<WorkflowManager>.

using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Tessa.Cards; using Tessa.Cards.Workflow; using Tessa.Extensions.Default.Shared; using Tessa.Extensions.Default.Shared.Workflow.TestProcess; using Tessa.Platform.Storage; using Tessa.Platform.Validation;

namespace Tessa.Extensions.Default.Server.Workflow.TestProcess { /// <summary> /// Класс, реализующий логику бизнес-процесса TestProcess. /// </summary> public sealed class TestWorkflowWorker : WorkflowTaskWorker<IWorkflowManager> { #region Constructors

public TestWorkflowWorker( IWorkflowManager manager, ICardRepository cardRepositoryToCreateTasks) : base(manager, cardRepositoryToCreateTasks) { }

#endregion

#region Private Task Helpers

/// <summary> /// Отправляет задание типа Тип1 с указанием переходов, /// выполняемых при завершении задания по каждому из вариантов завершения. /// </summary> /// <param name="completionTransitionA">Номер перехода, выполняемого при выборе варианта завершения А.</param> /// <param name="completionTransitionB">Номер перехода, выполняемого при выборе варианта завершения Б.</param> /// <param name="processInfo">Подпроцесс, в котором отправляется задание.</param> /// <param name="digest">Краткая информация по заданию, которую увидит пользователь.</param> /// <param name="roleID">Идентификатор роли, на которую отправляется задание.</param> /// <param name="roleName">Имя роли, на которую отправляется задание.</param> /// <param name="cancellationToken">Объект, посредством которого можно отменить асинхронную задачу.</param> /// <returns>Асинхронная задача.</returns> private Task SendTaskTypeOneAsync( int completionTransitionA, int completionTransitionB, IWorkflowProcessInfo processInfo, string digest, Guid roleID, string roleName, CancellationToken cancellationToken = default) => this.SendTaskAsync( DefaultTaskTypes.TestTask1TypeID, processInfo, digest, roleID, roleName, new Dictionary<string, object>(StringComparer.Ordinal) { { "A", completionTransitionA }, { "B", completionTransitionB }, }, cancellationToken: cancellationToken);

/// <summary> /// Отправляет задание типа Тип1 на роль текущего пользователя с указанием переходов, /// выполняемых при завершении задания по каждому из вариантов завершения. /// </summary> /// <param name="completionTransitionA">Номер перехода, выполняемого при выборе варианта завершения А.</param> /// <param name="completionTransitionB">Номер перехода, выполняемого при выборе варианта завершения Б.</param> /// <param name="processInfo">Подпроцесс, в котором отправляется задание.</param> /// <param name="digest">Краткая информация по заданию, которую увидит пользователь.</param> /// <param name="cancellationToken">Объект, посредством которого можно отменить асинхронную задачу.</param> /// <returns>Асинхронная задача.</returns> private Task SendTaskTypeOneAsync( int completionTransitionA, int completionTransitionB, IWorkflowProcessInfo processInfo, string digest, CancellationToken cancellationToken = default) => this.SendTaskTypeOneAsync( completionTransitionA, completionTransitionB, processInfo, digest, this.Manager.Session.User.ID, this.Manager.Session.User.Name, cancellationToken);

/// <summary> /// Отправляет задание типа Тип2 с указанием перехода, выполняемого при завершении задания. /// </summary> /// <param name="completionTransition">Номер перехода, выполняемого при завершении задания.</param> /// <param name="processInfo">Подпроцесс, в котором отправляется задание.</param> /// <param name="digest">Краткая информация по заданию, которую увидит пользователь.</param> /// <param name="roleID">Идентификатор роли, на которую отправляется задание.</param> /// <param name="roleName">Имя роли, на которую отправляется задание.</param> /// <param name="cancellationToken">Объект, посредством которого можно отменить асинхронную задачу.</param> /// <returns>Асинхронная задача.</returns> private Task SendTaskTypeTwoAsync( int completionTransition, IWorkflowProcessInfo processInfo, string digest, Guid roleID, string roleName, CancellationToken cancellationToken = default) => this.SendTaskAsync( DefaultTaskTypes.TestTask2TypeID, processInfo, digest, roleID, roleName, new Dictionary<string, object>(StringComparer.Ordinal) { { "Completion", completionTransition }, }, cancellationToken: cancellationToken);

/// <summary> /// Отправляет задание типа Тип2 на роль текущего пользователя с указанием перехода, /// выполняемого при завершении задания. /// </summary> /// <param name="completionTransition">Номер перехода, выполняемого при завершении задания.</param> /// <param name="processInfo">Подпроцесс, в котором отправляется задание.</param> /// <param name="digest">Краткая информация по заданию, которую увидит пользователь.</param> /// <param name="cancellationToken">Объект, посредством которого можно отменить асинхронную задачу.</param> /// <returns>Асинхронная задача.</returns> private Task SendTaskTypeTwoAsync( int completionTransition, IWorkflowProcessInfo processInfo, string digest, CancellationToken cancellationToken = default) => this.SendTaskTypeTwoAsync( completionTransition, processInfo, digest, this.Manager.Session.User.ID, this.Manager.Session.User.Name, cancellationToken);

#endregion

#region Base Overrides

/// <summary> /// Выполняет действия при запуске подпроцесса. /// </summary> /// <param name="processInfo">Информация по запускаемому подпроцессу.</param> /// <param name="cancellationToken">Объект, посредством которого можно отменить асинхронную задачу.</param> /// <returns>Асинхронная задача.</returns> protected override async Task StartProcessCoreAsync( IWorkflowProcessInfo processInfo, CancellationToken cancellationToken = default) { switch (processInfo.ProcessTypeName) { case TestProcessHelper.MainSubProcess: await this.Manager.InitCounterAsync(1, processInfo, initialValue: 2, cancellationToken: cancellationToken); await this.RenderStepAsync(1, processInfo, cancellationToken); await this.RenderStepAsync(2, processInfo, cancellationToken); break;

case TestProcessHelper.SubProcess1: await this.RenderStepAsync(1, processInfo, cancellationToken); break;

default: throw new ArgumentOutOfRangeException(nameof(processInfo.ProcessTypeName), processInfo.ProcessTypeName, null); } }

/// <summary> /// Выполняет действия при завершении подпроцесса. /// </summary> /// <param name="processInfo">Информация по завершаемому подпроцессу.</param> /// <param name="cancellationToken">Объект, посредством которого можно отменить асинхронную задачу.</param> /// <returns>Асинхронная задача.</returns> protected override async Task StopProcessCoreAsync( IWorkflowProcessInfo processInfo, CancellationToken cancellationToken = default) { switch (processInfo.ProcessTypeName) { case TestProcessHelper.MainSubProcess: // завершился бизнес-процесс, здесь можно перевести карточку в состоянию "Согласовано" break;

case TestProcessHelper.SubProcess1: await this.StopSubProcessWithCompletionAsync(processInfo, cancellationToken); break;

default: throw new ArgumentOutOfRangeException(nameof(processInfo.ProcessTypeName), processInfo.ProcessTypeName, null); } }

/// <summary> /// Выполняет действия при завершении задания. /// </summary> /// <param name="taskInfo">Информация по завершаемому заданию.</param> /// <param name="cancellationToken">Объект, посредством которого можно отменить асинхронную задачу.</param> /// <returns>Асинхронная задача.</returns> protected override async Task CompleteTaskCoreAsync( IWorkflowTaskInfo taskInfo, CancellationToken cancellationToken = default) { Guid typeID = taskInfo.Task.TypeID; if (typeID == DefaultTaskTypes.TestTask1TypeID) { Guid? optionID = taskInfo.Task.OptionID; if (optionID == DefaultCompletionOptions.OptionA) { // Тип1, Вариант А int transitionNumber = taskInfo.TaskParameters.Get<int>("A"); await this.RenderStepAsync(transitionNumber, taskInfo, cancellationToken); } else if (optionID == DefaultCompletionOptions.OptionB) { // Тип1, Вариант Б int transitionNumber = taskInfo.TaskParameters.Get<int>("B"); await this.RenderStepAsync(transitionNumber, taskInfo, cancellationToken); } } else if (typeID == DefaultTaskTypes.TestTask2TypeID) { // Тип2 int transitionNumber = taskInfo.TaskParameters.Get<int>("Completion"); await this.RenderStepAsync(transitionNumber, taskInfo, cancellationToken); } }

/// <summary> /// Обрабатывает внешний сигнал. Возвращает признак того, что сигнал известен и был обработан. /// </summary> /// <param name="signalInfo">Информация по сигналу и подпроцессу, для которого выполняется сигнал.</param> /// <param name="cancellationToken">Объект, посредством которого можно отменить асинхронную задачу.</param> /// <returns>Признак того, что сигнал известен и был обработан.</returns> protected override async Task<bool> ProcessSignalCoreAsync( IWorkflowSignalInfo signalInfo, CancellationToken cancellationToken = default) { if (signalInfo.Signal.Type != WorkflowSignalTypes.Default) { return await base.ProcessSignalCoreAsync(signalInfo, cancellationToken); }

switch (signalInfo.ProcessTypeName) { case TestProcessHelper.MainSubProcess: switch (signalInfo.Signal.Name) { case TestProcessHelper.TestSignal: await this.RenderStepCoreAsync(7, signalInfo, cancellationToken); return true; } break; }

return false; }

/// <summary> /// Выполняет переход. /// </summary> /// <param name="transitionNumber">Номер перехода.</param> /// <param name="processInfo">Информация по подпроцессу, в котором выполняется переход.</param> /// <param name="cancellationToken">Объект, посредством которого можно отменить асинхронную задачу.</param> /// <returns>Асинхронная задача.</returns> protected override async Task RenderStepCoreAsync( int transitionNumber, IWorkflowProcessInfo processInfo, CancellationToken cancellationToken = default) { switch (processInfo.ProcessTypeName) { case TestProcessHelper.MainSubProcess: switch (transitionNumber) { case 1: await this.SendTaskTypeOneAsync(3, 4, processInfo, "Первое задание в основном процессе", cancellationToken); break;

case 2: await this.StartSubProcessWithCompletionAsync( TestProcessHelper.SubProcess1, 4, processInfo, cancellationToken: cancellationToken); break;

case 3: await this.SendTaskTypeTwoAsync(4, processInfo, "Второе необязательное задание в основном процессе", cancellationToken); break;

case 4: if (await this.Manager.DecrementCounterAsync( 1, processInfo, cancellationToken: cancellationToken) == WorkflowCounterState.Finished) { await this.RenderStepAsync(5, processInfo, cancellationToken); } break;

case 5: await this.StartSubProcessWithCompletionAsync( TestProcessHelper.SubProcess1, 6, processInfo, cancellationToken: cancellationToken); break;

case 6: await this.StopProcessAsync(processInfo, cancellationToken); break;

case 7: this.Manager.ValidationResult.AddInfo(this, "Тестовый сигнал обработан"); break;

default: throw new ArgumentOutOfRangeException(nameof(transitionNumber), transitionNumber, null); } break;

case TestProcessHelper.SubProcess1: switch (transitionNumber) { case 1: await this.Manager.InitCounterAsync(1, processInfo, 3, cancellationToken); for (int i = 0; i < 3; i++) { await this.SendTaskTypeOneAsync(2, 3, processInfo, "Одно из трёх заданий в подпроцессе", cancellationToken); } break;

case 2: await this.RenderStepAsync(4, processInfo, cancellationToken); break;

case 3: await this.RenderStepAsync(4, processInfo, cancellationToken); break;

case 4: if (await this.Manager.DecrementCounterAsync( 1, processInfo, cancellationToken: cancellationToken) == WorkflowCounterState.Finished) { await this.RenderStepAsync(5, processInfo, cancellationToken); } break;

case 5: await this.StopProcessAsync(processInfo, cancellationToken); break;

default: throw new ArgumentOutOfRangeException(nameof(transitionNumber), transitionNumber, null); } break;

default: throw new ArgumentOutOfRangeException(nameof(processInfo.ProcessTypeName), processInfo.ProcessTypeName, null); } }

#endregion } }

Расширение на сохранение карточки следует унаследовать от класса WorkflowStoreExtension, переопределив в нём несколько методов (для определения собственного WorkflowManager’а потребуется дополнительно переопределить метод CreateManager).

using System; using System.Threading; using System.Threading.Tasks; using Tessa.Cards; using Tessa.Cards.ComponentModel; using Tessa.Cards.Extensions; using Tessa.Cards.Workflow; using Tessa.Extensions.Default.Server.Workflow.KrProcess; using Tessa.Extensions.Default.Shared; using Tessa.Extensions.Default.Shared.Workflow.TestProcess; using Unity;

namespace Tessa.Extensions.Default.Server.Workflow.TestProcess { public sealed class TestWorkflowStoreExtension : KrWorkflowStoreExtension { #region Constructors

public TestWorkflowStoreExtension( IKrTokenProvider krTokenProvider, [Dependency(CardRepositoryNames.DefaultWithoutTransaction)] ICardRepository cardRepositoryToCreateNextRequest, [Dependency(CardRepositoryNames.ExtendedWithoutTransaction)] ICardRepository cardRepositoryToStoreNextRequest, [Dependency(CardRepositoryNames.ExtendedWithoutTransaction)] ICardRepository cardRepositoryToCreateTasks, ICardTaskHistoryManager taskHistoryManager, ICardGetStrategy cardGetStrategy, IWorkflowQueueProcessor workflowQueueProcessor) : base( krTokenProvider, cardRepositoryToCreateNextRequest, cardRepositoryToStoreNextRequest, cardRepositoryToCreateTasks, taskHistoryManager, cardGetStrategy, workflowQueueProcessor) { }

#endregion

#region Base Overrides

protected override async ValueTask<bool> TaskIsAllowedAsync(CardTask task, ICardStoreExtensionContext context) { Guid taskTypeID = task.TypeID; return taskTypeID == DefaultTaskTypes.TestTask1TypeID || taskTypeID == DefaultTaskTypes.TestTask2TypeID; }

protected override async ValueTask<bool> CanHandleQueueItemAsync(WorkflowQueueItem queueItem, ICardStoreExtensionContext context) => TestProcessHelper.MainSubProcess == queueItem.Signal.ProcessTypeName;

protected override ValueTask<bool> CanStartProcessAsync(Guid? processID, string processName, ICardStoreExtensionContext context) { switch (processName) { case TestProcessHelper.ProcessName: return new ValueTask<bool>(true);

default: return new ValueTask<bool>(false); } }

protected override Task StartProcessAsync( Guid? processID, string processName, IWorkflowWorker workflowWorker, CancellationToken cancellationToken = default) { switch (processName) { case TestProcessHelper.ProcessName: return workflowWorker.StartProcessAsync( TestProcessHelper.MainSubProcess, newProcessID: processID, cancellationToken: cancellationToken);

default: throw new ArgumentOutOfRangeException(nameof(processName), processName, null); } }

protected override async ValueTask<IWorkflowWorker> CreateWorkerAsync( IWorkflowManager workflowManager, CancellationToken cancellationToken = default) => new TestWorkflowWorker(workflowManager, this.CardRepositoryToCreateTasks);

#endregion } }

В конструкторе расширения используются параметры:

  • cardRepositoryToCreateNextRequest – репозиторий карточек, используемый для создания запроса на дополнительное сохранение IWorkflowManager.NextRequest, в котором будут создаваться задания и изменяться поля карточки вследствие переходов в бизнес-процессе.

  • cardRepositoryToStoreNextRequest – репозиторий карточек, используемый для сохранения запроса IWorkflowManager.NextRequest, если такое сохранение требуется в соответствии со значением IWorkflowManager.NextRequestPending, а также в соответствии с наличием изменений внутри карточки NextRequest.Card. Необходимо использовать репозиторий без транзакции для этого параметра.

  • cardRepositoryToCreateTasks – репозиторий карточек, используемый для создания карточек заданий, которые затем помещаются в пакет карточки NextRequest.Card.

Расширение регистрируется в Unity следующим образом для типа карточек “Car”, при этом для первого параметра (cardRepositoryToCreateNextRequest) в конструкторе передаётся реализация репозитория карточек без транзакции и без расширений (чтобы не выполнялись такие расширения, как резервирование номера), а для остальных – без транзакции и с расширениями:

[Registrator] public sealed class Registrator : RegistratorBase { public override void RegisterUnity() { this.UnityContainer .RegisterType<TestWorkflowStoreExtension>(new PerResolveLifetimeManager()) ; }

public override void RegisterExtensions(IExtensionContainer extensionContainer) { extensionContainer .RegisterExtension<ICardStoreExtension, TestWorkflowStoreExtension>(x => x .WithOrder(ExtensionStage.AfterPlatform) .WithUnity(this.UnityContainer) .WhenCardTypes(DefaultCardTypes.CarTypeID)) ; } }

Для отображения плитки, запускающей согласование, в типе карточек “Car” используется расширение, сохраняющее карточку со специальным флагом, указываемым в методе SetStartingProcessName:

using System.Collections.Generic; using System.Threading.Tasks; using Tessa.Cards; using Tessa.Cards.Workflow; using Tessa.Extensions.Default.Shared.Workflow.TestProcess; using Tessa.Platform.Collections; using Tessa.UI; using Tessa.UI.Cards; using Tessa.UI.Tiles; using Tessa.UI.Tiles.Extensions;

namespace Tessa.Extensions.Default.Client.Tiles { /// <summary> /// Расширения для бизнес-процесса TestProcess. /// </summary> public sealed class TestProcessTileExtension : TileExtension { #region Evaluating Handlers

private static void EnableOnTestTypesAndNoProcesses(object sender, TileEvaluationEventArgs e) { ICardEditorModel editor = e.CurrentTile.Context.CardEditor; ICardModel model;

e.SetIsEnabledWithCollapsing( e.CurrentTile, editor != null && (model = editor.CardModel) != null && model.CardType.Flags.Has(CardTypeFlags.AllowTasks) && model.CardType.Name == "Car" && model.Card.StoreMode == CardStoreMode.Update && model.Card.Sections["WorkflowProcesses"].Rows.Count == 0); }

private static void EnableOnTestTypesAndHasProcesses(object sender, TileEvaluationEventArgs e) { ICardEditorModel editor = e.CurrentTile.Context.CardEditor; ICardModel model;

e.SetIsEnabledWithCollapsing( e.CurrentTile, editor != null && (model = editor.CardModel) != null && model.CardType.Flags.Has(CardTypeFlags.AllowTasks) && model.CardType.Name == "Car" && model.Card.StoreMode == CardStoreMode.Update && model.Card.Sections["WorkflowProcesses"].Rows.Count > 0); }

#endregion

#region Command Actions

private static async void StartTestProcessActionAsync(object parameters) { IUIContext context = UIContext.Current; ICardEditorModel editor = context.CardEditor;

if (editor == null || editor.CardModel == null) { return; }

await editor.SaveCardAsync( context, new Dictionary<string, object>() .SetStartingProcessName(TestProcessHelper.ProcessName)); }

private static async void SendTestSignalActionAsync(object parameters) { IUIContext context = UIContext.Current; ICardEditorModel editor = context.CardEditor;

if (editor == null || editor.CardModel == null) { return; }

await editor.SaveCardAsync( context, request: new CardSavingRequest(cardModifierActionAsync: async (card, ct) => { WorkflowQueue queue = card.GetWorkflowQueue();

queue.AddSignal( TestProcessHelper.MainSubProcess, TestProcessHelper.TestSignal); })); }

#endregion

#region Base Overrides

public override Task InitializingGlobal(ITileGlobalExtensionContext context) { ITileContextSource contextSource = context.Workspace.LeftPanel;

context.Workspace.LeftPanel.Tiles.AddRange( new Tile( "StartTestProcess", TileHelper.SplitCaption("$KrTest_TestApprovalTile"), context.Icons.Get("Thin127"), contextSource, new DelegateCommand(StartTestProcessActionAsync), TileGroups.Cards, order: 6, evaluating: EnableOnTestTypesAndNoProcesses),

new Tile( "SendTestSignal", TileHelper.SplitCaption("$KrTest_TestSignalTile"), context.Icons.Get("Thin229"), contextSource, new DelegateCommand(SendTestSignalActionAsync), TileGroups.Cards, order: 6, evaluating: EnableOnTestTypesAndHasProcesses)) ;

return Task.CompletedTask; }

#endregion } }

Регистрация расширения выполняется на клиенте:

extensionContainer .RegisterExtension<ITileGlobalExtension, TestProcessTileExtension>(x => x .WithOrder(ExtensionStage.AfterPlatform, 7) .WithUnity(unityContainer .RegisterType<TestProcessTileExtension>(new ContainerControlledLifetimeManager())));

Класс, содержащий константы, используемые выше, доступен на клиенте и на сервере. Его код приведён ниже.

namespace Tessa.Extensions.Default.Shared.Workflow.TestProcess { public static class TestProcessHelper { #region Process Name

/// <summary> /// Имя процесса, используемое для его запуска. /// </summary> public const string ProcessName = "TestProcess";

#endregion

#region SubProcess Names

/// <summary> /// Имя основного подпроцесса, который доступен один на карточку. /// </summary> public const string MainSubProcess = "Main";

/// <summary> /// Имя подпроцесса "Подпроцесс 1", может запускаться несколько раз из основного подпроцесса <see cref="MainSubProcess"/>. /// </summary> public const string SubProcess1 = "Process1";

/// <summary> /// Имя тестового сигнала. Отправляется на основной подпроцесс <see cref="MainSubProcess"/>. /// </summary> public const string TestSignal = "TestSignal";

#endregion } }

Установка информации по бизнес-процессу в истории заданий

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

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

Такая информация содержит следующие поля:

  • ProcessID (Guid) – идентификатор бизнес-процесса, по которому определяется, что записи в истории принадлежат одному и тому же процессу, например, одной и той же резолюции.

  • ProcessName (string) – отображаемое пользователю имя бизнес-процесса. Может также содержать дополнительную информацию по экземпляру процесса.

  • ProcessKind (string) – имя типа бизнес-процесса, определяющее принадлежность бизнес-процесса к некоторому виду процессов, такому как “резолюции”. Поле может использоваться для группировки или не быть заданным вообще.

По умолчанию все поля из списка равны null. Для того, чтобы при завершении задания или при его сохранении с созданием записи в истории поля установились, их необходимо задать в объекте CardTask следующим образом:

CardTask task = ...;

// для первого задания в процессе создаём новый ID task.ProcessID = Guid.NewGuid(); task.ProcessName = "Резолюция - Иванов И., январь 2015 г."; task.ProcessKind = "Резолюция";

При добавлении записи в историю заданий вручную через объект CardTaskHistoryItem поля требуется установить в этом объекте:

CardTask historyItem = card.TaskHistory.Add(); historyItem.RowID = task.RowID; historyItem.State = CardTaskHistoryState.Insert; // устанавливаем другие свойства записи

historyItem.ProcessID = Guid.NewGuid(); historyItem.ProcessName = "Резолюция - Иванов И., январь 2015 г."; historyItem.ProcessKind = "Резолюция";

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

foreach (CardTaskHistoryItem historyItem in card.TaskHistory) { TessaDialog.ShowMessage(string.Format( "Информация по бизнес-процессу для записи RowID={0}:" + " ProcessID={1}, ProcessName=\"{2}\", ProcessKind=\"{3}\"", historyItem.RowID, historyItem.ProcessID, historyItem.ProcessName, historyItem.ProcessKind)); }

Back to top