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

Маршруты

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

Программный запуск маршрута с фиксированным набором групп

Для запуска процесса в коде расширений необходимо собрать процесс с помощью KrProcessBuilder, а затем запустить полученный процесс с помощью IKrProcessLauncher, который можно получить из Unity-контейнера. Запустить можно маршрут, собранный в вторичный процесс.

Описание параметров используемых при запуске процессов

Параметры, используемые при запуске процесса, передаются в параметр specificParameters метода IKrProcessLauncher.LaunchAsync(KrProcessInstance krProcess, ICardExtensionContext cardContext = null, IKrProcessLauncherSpecificParameters specificParameters = null, CancellationToken cancellationToken = default).

Классы, содержащие параметры запуска процесса, реализуют интерфейс IKrProcessLauncherSpecificParameters. Существует следующие типовые реализации, используемые при запуске процесса:

  1. В серверных расширениях – KrProcessServerLauncher.SpecificParameters;
  2. В клиентских расширениях имеющих доступ к зависимостям определённым в сборке Tessa.UI – KrProcessClientUILauncher.SpecificParameters;
  3. В клиентских расширениях не имеющих доступа к зависимостям определённым в сборке Tessa.UI – KrProcessClientLauncher.SpecificParameters.

В серверных расширениях

Дополнительные параметры запуска процесса KrProcessServerLauncher.SpecificParameters:

  • MainCardAccessStrategy – стратегия доступа к основной карточке.

  • RaiseErrorWhenExecutionIsForbidden – флаг, показывающий, следует ли создавать ошибку, если процесс не может быть выполнен из-за ограничений (параметр вторичного процесса “Сообщение при невозможности выполнения процесса”).

  • UseSameRequest – значение, показывающее, что процесс требуется запустить в текущем выполнении запроса. С учетом данной особенности процесс может быть запланирован только в BeforeRequest. Если не указать, процесс будет запущен с помощью вложенного сохранения. В общем случае запускать необходимо именно во вложенном сохранении. Для работы UseSameRequest необходимо передать контекст расширения на сохранение ICardStoreExtensionContext.

Пример запуска процесса в серверных расширениях

var process = KrProcessBuilder .CreateProcess() .SetProcess(/*Идентификатор карточки вторичного процесса*/) // Карточка необходима для локальных процессов. // Локальный или глобальный процесс - настройка в карточке вторичного процесса .SetCard(context.Request.Card.ID) // Опционально можно указать ProcessInfo. // В скриптах будет доступно в ProcessInfo или WorkflowProcess.Info. // Позволяет запускать процесс с параметрами. .SetProcessInfo( new Dictionary<string, object>(StringComparer.Ordinal) { { KrConstants.Keys.NotMessageHasNoActiveStages, BooleanBoxes.True } // Опциональный параметр процесса позволяющий отключить сообщение при отсутствии этапов доступных для выполнения. }) .Build();

// Опционально указываются особенности запуска процесса. var specificParameters = new KrProcessServerLauncher.SpecificParameters { UseSameRequest = true };

// context - реализация ICardExtensionContext. В контексты расширений ICardStoreExtensionContext и ICardRequestExtensionContext будут устанавливаться клиентские команды. // Для работы `UseSameRequest` необходимо передать контекст расширения на сохранение ICardStoreExtensionContext. var launchResult = await this.launcher.LaunchAsync(process, context, specificParameters);

var validationResult = launchResult.ValidationResult; var startedProcessID = launchResult.ProcessID;

В клиентских расширениях имеющих доступ к зависимостям определённым в сборке Tessa.UI

Дополнительные параметры запуска процесса KrProcessClientUILauncher.SpecificParameters:

  • UseCurrentCardEditor – значение, показывающее, следует ли использовать текущий ICardEditorModel или нет. Приоритет выше, чем у свойства CardEditor.

  • CardEditorICardEditorModel который следует использовать. Приоритет ниже, чем у UseCurrentCardEditor.

  • RequestInfo – дополнительная информация, передаваемая в запросе на сохранение карточки для расширений. Данные должны быть сериализуемых типов.

  • RaiseErrorWhenExecutionIsForbidden – флаг, показывающий, следует ли создавать ошибку, если процесс не может быть выполнен из-за ограничений (параметр вторичного процесса “Сообщение при невозможности выполнения процесса”).

Пример запуска процесса в клиентских расширениях

var process = KrProcessBuilder .CreateProcess() .SetProcess(/*Идентификатор карточки вторичного процесса*/) .SetCard(cardID) .SetProcessInfo( new Dictionary<string, object>(StringComparer.Ordinal) { { KrConstants.Keys.NotMessageHasNoActiveStages, BooleanBoxes.True } // Опциональный параметр процесса позволяющий отключить сообщение при отсутствии этапов доступных для выполнения. }) .Build();

var launchResult = await this.launcher.LaunchAsync(process, context);

Пример запуска процесса в клиентских расширениях с использованием текущего ICardEditorModel

ICardEditorModel editor = uiContext.CardEditor; var process = KrProcessBuilder .CreateProcess() .SetProcess(/*Идентификатор карточки вторичного процесса*/) .SetCard(cardID) .SetProcessInfo( new Dictionary<string, object>(StringComparer.Ordinal) { { KrConstants.Keys.NotMessageHasNoActiveStages, BooleanBoxes.True } // Опциональный параметр процесса позволяющий отключить сообщение при отсутствии этапов доступных для выполнения. }) .Build(); var launchResult = await this.launcher.LaunchWithCardEditorAsync(process, editor);

Пример запуска процесса при задании параметров запускаемого процесса в клиентских расширениях

var card = ...; // Карточка по которой должен быть запущен процесс. ICardRepository cardRepository = ...; // Репозиторий для управления карточками.

var process = KrProcessBuilder .CreateProcess() .SetProcess(/*Идентификатор карточки вторичного процесса*/) // Карточка необходима для локальных процессов. // Локальный или глобальный процесс - настройка в карточке вторичного процесса .SetCard(context.Request.Card.ID) // Опционально можно указать ProcessInfo. // В скриптах будет доступно в ProcessInfo или WorkflowProcess.Info. // Позволяет запускать процесс с параметрами. .SetProcessInfo( new Dictionary<string, object>(StringComparer.Ordinal) { { KrConstants.Keys.NotMessageHasNoActiveStages, BooleanBoxes.True } // Опциональный параметр процесса позволяющий отключить сообщение при отсутствии этапов доступных для выполнения. }) .Build();

var request = new CardStoreRequest() { Card = card }; var info = request.Info; info.SetKrProcessInstance(process); info[KrConstants.RaiseErrorWhenExecutionIsForbidden] = BooleanBoxes.True; // Значение, показывающее, следует ли создавать ошибку, если процесс не может быть выполнен из-за ограничений (параметр вторичного процесса "Сообщение при невозможности выполнения процесса"). Если данное поведение не требуется, то данный параметр задавать не следует.

var response = await cardRepository.StoreAsync(storeRequest, cancellationToken);

В клиентских расширениях не имеющих доступ к зависимостям определённым в сборке Tessa.UI

Дополнительные параметры запуска процесса KrProcessClientLauncher.SpecificParameters:

  • RequestInfo – дополнительная информация, передаваемая в запросе на сохранение карточки для расширений. Данные должны быть сериализуемых типов.

  • RaiseErrorWhenExecutionIsForbidden – флаг, показывающий, следует ли создавать ошибку, если процесс не может быть выполнен из-за ограничений (параметр вторичного процесса “Сообщение при невозможности выполнения процесса”).

Пример запуска процесса

var process = KrProcessBuilder .CreateProcess() .SetProcess(/*Идентификатор карточки вторичного процесса*/) .SetCard(cardID) .SetProcessInfo( new Dictionary<string, object>(StringComparer.Ordinal) { { KrConstants.Keys.NotMessageHasNoActiveStages, BooleanBoxes.True } // Опциональный параметр процесса позволяющий отключить сообщение при отсутствии этапов доступных для выполнения. }) .Build();

var launchResult = await this.launcher.LaunchAsync(process, context);

Дескриптор типа этапа

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

Список полей дескриптора:

  • ID - идентификатор типа этапа.

  • Caption - название типа этапа.

  • DefaultStageName - стандартное название типа этапа, которое будет подставляться в KrStages.Name при каждом создании новой строки с этапом.

  • SettingsCardTypeID - идентификатор типа карточки настроек.

  • PerformerUsageMode - режим использования исполнителей. Возможны следующие значения:

    • None - исполнители в этапе не используются.

    • Single - используется одиночный исполнитель. В редакторе этапа будет отображен элемент управления “ссылка” для указания роли, а также в объекте этапа Stage свойство Performer.

    • Multiple - используется несколько исполнителей. В редакторе этапа будет отображен элемент управления “список”, а также в объекте Stage свойство Performers.

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

  • UseTimeLimit - использовать поле Срок. В редакторе строки этапа будет отображен элемент управления для ввода срока. В объектной модели этапа введенная информация доступна в свойстве TimeLimit.

  • SupportedModes - список поддерживаемых этапом режимов. Этапы могут поддерживать следующие режимы:

    • KrProcessRunnerMode.Sync - этап поддерживает синхронный режим, т.е. выполнение за один запуск. Рекомендуется указывать всегда.

    • KrProcessRunnerMode.Async - этап поддерживает асинхронный режим. Рекомендуется указывать, если этап отправляет задания.

  • CanBeSkipped - разрешено пропускать этап.

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

public static class CustomStageTypeDescriptors { public static readonly StageTypeDescriptor ApprovalDescriptor = StageTypeDescriptor.Create(b => { b.ID = new Guid(0x185610E1, 0x6AB0, 0x64E, 0x94, 0x29, 0x4C, 0x52, 0x98, 0x4, 0xDF, 0xE4); b.Caption = "$KrStages_Approval"; b.SettingsCardTypeID = new Guid(0x4A377758, 0x2366, 0x47E9, 0x98, 0xAC, 0xC5, 0xF5, 0x53, 0x97, 0x42, 0x36); b.PerformerUsageMode = PerformerUsageMode.Multiple; b.PerformerIsRequired = true; b.UseTimeLimit = true; b.SupportedModes.Add(KrProcessRunnerMode.Async); b.CanBeSkipped = true; }); }

Настройки типа этапа

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

Note

Для упрощения работы с маршрутами мы рекомендуем соблюдать следующие соглашение наименования:

  • Имя типа карточки, содержащей настройки типа этапа, должно оканчиваться на StageTypeSettings.

    Например, KrApprovalStageTypeSettings – тип карточки настроек этапа “Согласование”.

  • Имя типа карточки, содержащей общие настройки для нескольких типов этапов, должно оканчиваться на Settings.

    Например, KrAuthorSettings – тип карточки настроек определяющих автора задания создаваемого этапом.

В качестве примера рассмотрим карточку настроек типа этапа согласование.

  • Карточка настроек этапа в редакторе TessaAdmin

  • Настройки этапа

Для связи карточки настроек и типа этапа необходимо указать идентификатор типа карточки в дескрипторе типа этапа в свойстве SettingsCardTypeID.

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

Поддерживаются следующие тэги:

  • Runtime - элемент управления видимый только в документе;

  • DesignTime - элемент управления видимый только в шаблоне этапов;

  • RuntimeReadonly - элемент управления неизменяемый в документе;

  • DesignTimeReadonly - элемент управления неизменяемый в шаблоне этапов;

Типы карточек, содержащие настройки стандартных типов этапов

Тип этапа
Название типа карточки
Ветвление KrForkStageTypeSettings
Диалог KrDialogStageTypeSettings
Доработка KrEditStageTypeSettings
Задача KrResolutionStageTypeSettings
Настраиваемое задание KrUniversalTaskStageTypeSettings
Ознакомление KrAcquaintanceStageTypeSettings
Подписание KrSigningStageTypeSettings
Регистрация KrRegistrationStageTypeSettings
Смена состояния KrChangeStateStageTypeSettings
Согласование KrApprovalStageTypeSettings
Создание карточки KrCreateCardStageTypeSettings
Создать файл по шаблону KrAddFileFromTemplateStageTypeSettings
Типизированное задание KrTypedTaskStageTypeSettings
Уведомление KrNotificationStageTypeSettings
Управление ветвлением KrForkManagementStageTypeSettings
Управление историей KrHistoryManagementStageTypeSettings
Управление процессом KrProcessManagementStageTypeSettings

Общие типы карточек, содержащие настройки стандартных типов этапов

Название типа карточки
Описание
KrAuthorSettings Содержит поле для указания автора задания. Используется, если в дескрипторе типа этапа задана настройка CanOverrideAuthor.
KrPerformersSettings Содержит поля для задания исполнителя задания. Используется, если в дескрипторе типа этапа задана настройка PerformerUsageMode, значение которой отлично от PerformerUsageMode.None.
KrTaskKindSettings Содержит поле для задания типа задания. Используется, если в дескрипторе типа этапа задана настройка UseTaskKind.

Warning

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

Обработчик типа этапа

Основная логика процесса реализуется в обработчике этапа. Обработчик этапа - это класс, который наследует StageTypeHandlerBase. В нем предусмотрены следующие методы:

  • HandleStageStartAsync(context) - запуск этапа. В данном методе отправляются задания в асинхронных этапах и выполняется вся логика в синхронных этапах.

  • HandleTaskCompletionAsync(context) - завершение задания, отправленного этапом. В контексте содержится вся информация о завершаемом задании, в т.ч. информация IWorkflowTaskInfo из WorkflowAPI.

  • HandleTaskReinstateAsync(context) - возврат задания на доработку.

  • HandleSignalAsync(context) - обработка полученного сигнала WorkflowAPI. Сигналы позволяют реализовать внешнее воздействие на этап. Например, при каждом получении сигнала определенного типа этап должен отправлять дополнительное задание на роль, указанную в исполнителях.

  • HandleStageInterruptAsync(context) - один из важнейших методов обработчика этапа. С помощью этого метода подсистема маршрутов сообщает этапу о том, что ему необходимо прерваться, утилизировав все используемые ресурсы (отзыв заданий и др.). Метод возвращает значение типа Task<bool>:

    • true - этап полностью прерван и дополнительных вложенных запросов на завершение заданий НЕ требуется.

    • false - для завершения этапа необходим дополнительный вложенный запрос для завершения заданий. + Для примера рассмотрим прерывание этапа с 4мя заданиями, которые будут отзываться по 2 за запрос. Это учебный пример и на практике можно отзывать все задания за один запрос.

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

Все методы, кроме HandleStageInterruptAsync возвращают результат типа StageHandlerResult. С помощью результата происходит взаимодействие с подсистемой маршрутов. Возможны следующие результаты выполнения этапа:

  • StageHandlerResult.EmptyResult - метод-обработчик не знает, как реагировать и сообщает об этом подсистеме маршрутов. На практике поведение аналогично StageHandlerResult.EmptyResult, однако позволяет отличить, выполнил ли обработчик необходимые действия или нет.

  • StageHandlerResult.InProgressResult - этап находится в процессе выполнения, состояние этапа будет выставлено в “Активен”. Допустимо использовать только в асинхронных этапах.

  • StageHandlerResult.CompleteResult- этап будет переведен в состояние “Завершен” и управление будет передано следующему этапу маршрута.

  • StageHandlerResult.SkipResult- этап будет переведен в состояние “Пропущен” и управление будет передано следующему этапу маршрута.

  • StageHandlerResult.GroupTransition(Guid stageGroupID, bool keepStageStates = false)- выполнить переход на группу этапов. Если указанная группа существует, то будет произведен пересчет этапов и управление передано первому этапу в группе, иначе будет запущен следующий этап.

  • StageHandlerResult.Transition(Guid stageRowID, bool keepStageStates = false)- выполнить переход на этап. Если указанный этап не существует, то перейти на следующий.

  • StageHandlerResult.SkipProcessResult - пропустить процесс с переводом оставшихся этапов в состояние “Пропущен”.

  • StageHandlerResult.CancelProcessResult - отменить процесс с переводом всех этапов в состояние “Не запущен”.

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

using System; using System.Linq; using System.Threading.Tasks; using Tessa.Cards; using Tessa.Cards.ComponentModel; using Tessa.Cards.Extensions; using Tessa.Cards.Numbers; using Tessa.Cards.Workflow; using Tessa.Extensions.Default.Server.Workflow.KrProcess.Scope; using Tessa.Extensions.Default.Shared; using Tessa.Extensions.Default.Shared.Workflow.KrProcess; using Tessa.Platform.Data; using Tessa.Platform.Runtime; using Tessa.Platform.Storage; using Tessa.Platform.Validation; using static Tessa.Extensions.Default.Shared.Workflow.KrProcess.KrConstants;

namespace Tessa.Extensions.Default.Server.Workflow.KrProcess.Workflow.Handlers { public sealed class RegistrationStageTypeHandler : StageTypeHandlerBase { #region fields

private readonly INumberDirectorContainer numberDirectorContainer;

private readonly IKrScope krScope;

private readonly ISession session;

private readonly ICardMetadata cardMetadata;

private readonly IDbScope dbScope;

private readonly ICardGetStrategy cardGetStrategy;

#endregion

#region constuctor

public RegistrationStageTypeHandler( INumberDirectorContainer numberDirectorContainer, IKrScope krScope, ISession session, ICardMetadata cardMetadata, IDbScope dbScope, ICardGetStrategy cardGetStrategy) { this.numberDirectorContainer = numberDirectorContainer; this.krScope = krScope; this.session = session; this.cardMetadata = cardMetadata; this.dbScope = dbScope; this.cardGetStrategy = cardGetStrategy; }

#endregion

#region public

public override Task<StageHandlerResult> HandleStageStartAsync( IStageTypeHandlerContext context) { // При запуске этапа определяем, в каком режиме сейчас идет выполнение switch (context.RunnerMode) { case KrProcessRunnerMode.Sync: // Выполнение в синхронном режиме, отправка заданий запрещена // Выполняем регистрацию return this.SyncRegistrationAsync(context); case KrProcessRunnerMode.Async: // Выполнение в асинхронном режиме, отправляем задание регистрации return this.AsyncRegistrationAsync(context); default: throw new ArgumentOutOfRangeException(); } }

public override async Task<StageHandlerResult> HandleTaskCompletionAsync( IStageTypeHandlerContext context) { // Завершение задания регистрации // Вся информация о задании доступна в контексте var taskInfo = context.TaskInfo; var taskType = taskInfo.Task.TypeID; var optionID = taskInfo.Task.OptionID ?? Guid.Empty;

if (taskType == DefaultTaskTypes.KrRegistrationTypeID && optionID == DefaultCompletionOptions.RegisterDocument) { // Вариант завершения "Зарегистрировать" // Удаляем задание из списка активных await context.WorkflowAPI.TryRemoveActiveTaskAsync(taskInfo.Task.RowID, context.CancellationToken); // Проводим регистрацию документа await this.SyncRegistrationAsync(context, taskInfo); // Сообщаем подсистеме маршрутов о том, что работа этапа завершена. return StageHandlerResult.CompleteResult; }

throw new InvalidOperationException(); }

public override Task<bool> HandleStageInterruptAsync(IStageTypeHandlerContext context) => this.InterruptSingleTaskAsync(context);

#endregion

#region private

private async Task<StageHandlerResult> SyncRegistrationAsync( IStageTypeHandlerContext context, IWorkflowTaskInfo taskInfo = null) { // Непосредственная регистрация карточки. if (context.MainCardID.HasValue && context.MainCardTypeID.HasValue) { var cardType = (await this.cardMetadata.GetCardTypesAsync(context.CancellationToken))[context.MainCardTypeID.Value]; var mainCard = await this.krScope.GetMainCardAsync(context.MainCardID.Value, cancellationToken: context.CancellationToken); var numberProvider = this.numberDirectorContainer.GetProvider(context.MainCardTypeID); var numberDirector = numberProvider.GetDirector(); var numberComposer = numberProvider.GetComposer(); var numberContext = await numberDirector.CreateContextAsync( numberComposer, mainCard, cardType, context.CardExtensionContext is ICardStoreExtensionContext storeContext ? storeContext.Request.Info : null, context.CardExtensionContext, transactionMode: NumberTransactionMode.WithoutTransaction, context.CancellationToken);

// выделение номера при регистрации await numberDirector.NotifyOnRegisteringCardAsync(numberContext, context.CancellationToken); context.ValidationResult.Add(numberContext.ValidationResult); } context.WorkflowProcess.State = KrState.Registered; return StageHandlerResult.CompleteResult; }

private async Task<StageHandlerResult> AsyncRegistrationAsync( IStageTypeHandlerContext context) { var api = context.WorkflowAPI;

// Получаем исполнителя, указанного в настройках этапа. // Аналогичное действие можно выполнить с помощью context.Stage.Performer var performerID = context.Stage.SettingsStorage.TryGet<Guid?>(KrSinglePerformerVirtual.PerformerID); var performerName = context.Stage.SettingsStorage.TryGet<string>(KrSinglePerformerVirtual.PerformerName); if (!performerID.HasValue || string.IsNullOrWhiteSpace(performerName)) { context.ValidationResult.AddError(this, "Performer not specified"); return StageHandlerResult.EmptyResult; }

// Установка в карточке состояния "На регистрации" context.WorkflowProcess.State = KrState.Registration;

// Отправка задания регистрации var taskInfo = await api.SendTaskAsync( DefaultTaskTypes.KrRegistrationTypeID, context.Stage.Name, performerID.Value, performerName, cancellationToken: context.CancellationToken); // Добавление задания в список активных заданий, // которые будут отображатся в таблице над заданиями. await api.AddActiveTaskAsync(taskInfo.Task.RowID, context.CancellationToken);

// Результат говорит подсистеме маршрутов о том, что этап находится в процессе выполнения return StageHandlerResult.InProgressResult; }

private async Task<bool> InterruptSingleTaskAsync( IStageTypeHandlerContext context) { if (context.ProcessInfo is null || context.MainCardID is null) { return true; } var cardID = context.MainCardID.Value;

var db = this.dbScope.Db; // Получение списка заданий из таблицы WorkflowTasks var currentTasks = await db.SetCommand( dbScope.BuilderFactory .Select().Top(2) .C("RowID") .From("WorkflowTasks").NoLock() .Where().C("ProcessRowID").Equals().P("pid") .Limit(2) .Build(), db.Parameter("pid", context.ProcessInfo.ProcessID)) .LogCommand() .ExecuteListAsync<Guid>(context.CancellationToken);

// Метод поддерживает только одно задание для отзыва. switch (currentTasks.Count) { case 0: // Заданий нет, прерывание этапа завершено. return true; case 1: // Задание есть, его нужно отозвать. return await this.RevokeTaskAsync(cardID, currentTasks[0], context); default: throw new InvalidOperationException("More than one task."); } }

private async Task<bool> RevokeTaskAsync( Guid cardID, Guid taskID, IStageTypeHandlerContext context) { var validationResult = context.ValidationResult; var card = await this.krScope.GetMainCardAsync(cardID, cancellationToken: context.CancellationToken); var cardTasks = card.TryGetTasks(); if (cardTasks is null || cardTasks.All(p => p.RowID != taskID)) { var db = this.dbScope.Db; var taskContexts = await this.cardGetStrategy.TryLoadTaskInstancesAsync( card.ID, card, db, this.cardMetadata, validationResult, this.session.User.ID, getTaskMode: CardGetTaskMode.All, loadCalendarInfo: false, taskRowIDList: new[] { taskID }, cancellationToken: context.CancellationToken); foreach (var taskContext in taskContexts) { await this.cardGetStrategy.LoadSectionsAsync(taskContext, context.CancellationToken); } }

var task = cardTasks?.FirstOrDefault(p => p.RowID == taskID); if (task is null) { return true; }

await context.WorkflowAPI.TryRemoveActiveTaskAsync(task.RowID, context.CancellationToken); task.Action = CardTaskAction.Complete; task.State = CardRowState.Deleted; task.Flags = task.Flags & ~CardTaskFlags.Locked | CardTaskFlags.UnlockedForAuthor | CardTaskFlags.HistoryItemCreated; task.OptionID = DefaultCompletionOptions.Cancel; return false; }

#endregion } }

Для ассоциации обработчика с определённым типом этапа необходимо провести регистрацию с указанием дескриптора.

using Tessa.Extensions.Default.Shared; using Tessa.Extensions.Default.Shared.Workflow.KrProcess; using Tessa.Platform; using Unity; using Unity.Lifetime;

namespace Tessa.Extensions.Default.Server.Workflow.KrProcess.Workflow.Handlers { [Registrator] public sealed class Registrator : RegistratorBase { public override void RegisterUnity() { this.UnityContainer .RegisterType<RegistrationStageTypeHandler>(new ContainerControlledLifetimeManager()) ; }

public override void FinalizeRegistration() { this.UnityContainer .TryResolve<IKrProcessContainer>() ? // Связываем дескриптор типа этапа с типом обработчика .RegisterHandler<RegistrationStageTypeHandler>(StageTypeDescriptors.RegistrationDescriptor)

// Указываем тип задания, который будет использоваться в процессе в этапе регистрации. .RegisterTaskType(DefaultTaskTypes.KrRegistrationTypeID); } } }

Чтобы тип этапа стал доступен в диалоге выбора при добавлении нового этапа в таблицу необходимо добавить запись в перечисление KrProcessStageTypes, причем идентификатор указать такой же, как и в дескрипторе. Помимо этого, у пользователя должны быть права на добавление типа этапа в группу, что настраивается в карточке типового решения на вкладке “Настройки этапов маршрута”.

Для незначительного изменения настроек существующего этапа с возможным переиспользованием его обработчика можно добавить фильтр типа этапа по дескриптору с помощью метода IKrProcessContainer.AddFilter(filter), где фильтр StageTypeFilter.Exclude(handlerID). Данный метод должен вызываться при регистрации обработчиков типов этапов в регистраторе. После этого можно создать собственный дескриптор с измененными настройками и связать его со старым обработчиком. Таким образом, можно изменить внешний вид и настройки этапа без модификации существующего кода.

Создание дополнительного сценария этапа

Дополнительный сценарий этапа позволяет предоставить дополнительный контекст и методы, которые будут доступны только в этом сценарии и управляющие поведением этапа в определяемый разработчиком момент времени. Примерами таких сценариев являются сценарии используемые в типовых этапах, например, сценарий изменения уведомления в этапе “Уведомление”.

Рассмотрим на примере создания нового этапа содержащего дополнительный сценарий.

  1. Создание новой строковой виртуальной секции для хранения параметров этапа.

    Колонки

    Имя
    Тип
    Digest String(Max) Null
    Script String(Max) Null
  2. Создание нового типа карточки содержащей параметры этапа.

    В примере идентификатор типа карточки: 655e95f7-1730-4c6b-b42d-b2cac312946e.

  3. Создание дескриптор типа для нового этапа.

    public static class CustomStageTypeDescriptors { public static readonly StageTypeDescriptor ExampleStageDescriptor = StageTypeDescriptor.Create(b => { b.ID = new Guid(0xb4ba4a2c, 0x162b, 0x4c2b, 0xa7, 0x6a, 0x79, 0x10, 0x1d, 0x46, 0xdf, 0xc1); b.Caption = "$Edu_KrStages_ExampleStage"; b.SettingsCardTypeID = new Guid(0x655e95f7, 0x1730, 0x4c6b, 0xb4, 0x2d, 0xb2, 0xca, 0xc3, 0x12, 0x94, 0x6e); b.SupportedModes.AddRange(new[] { KrProcessRunnerMode.Async, KrProcessRunnerMode.Sync }); }); }

  4. Создание статического класса содержащего константы используемые в примере.

    using Tessa.Extensions.Default.Shared.Workflow.KrProcess;

    namespace Tessa.Extensions.Shared.Workflow.KrProcess { /// <summary> /// Предоставляет константы используемые в примере этапа. /// </summary> public static class EduConstants { public static class KrExampleStageSettingsVirtual { public const string Name = nameof(KrExampleStageSettingsVirtual); public static readonly string Digest = StageTypeSettingsNaming.PlainColumnName(Name, nameof(Digest)); public static readonly string Script = StageTypeSettingsNaming.PlainColumnName(Name, nameof(Script)); } } }

  5. Создание обработчика типа этапа.

    using System; using System.Threading.Tasks; using Tessa.Extensions.Default.Server.Workflow.KrCompilers; using Tessa.Extensions.Default.Server.Workflow.KrProcess.Workflow.Handlers; using Tessa.Extensions.Shared.Workflow.KrProcess; using Tessa.Extensions.Shared.Workflow.KrProcess.Workflow; using Tessa.Platform.Storage; using Tessa.Platform.Validation; using Unity;

    namespace Tessa.Extensions.Server.Workflow.KrProcess.Workflow.Handlers { /// <summary> /// Обработчик типа этапа <see cref="CustomStageTypeDescriptors.ExampleStageDescriptor"/>. /// </summary> public class ExampleStageHandler : StageTypeHandlerBase { #region Nested Types

    /// <summary> /// Предоставляет дополнительные данные доступные в методе <see cref="MethodName"/>. /// </summary> public class ScriptContext { /// <summary> /// Возвращает значение параметра этапа "Дайджест". /// </summary> public string Digest { get; }

    /// <summary> /// Инициализирует новый экземпляр класса <see cref="ScriptContext"/>. /// </summary> /// <param name="digest">Значение параметра этапа "Дайджест".</param> public ScriptContext( string digest) { this.Digest = digest; } }

    #endregion

    #region Constants And Static Fields

    /// <summary> /// Имя дополнительного метода. /// </summary> public static readonly string MethodName = "TestMethod";

    /// <summary> /// Отображаемое имя дополнительного метода. /// </summary> public static readonly string MethodDisplayName = "$Edu_KrStages_ExampleStage_TestMethod";

    /// <summary> /// Имя параметра метода <see cref="MethodName"/>. /// </summary> public static readonly string MethodParameterName = "context";

    /// <summary> /// Тип параметра <see cref="MethodParameterName"/>. /// </summary> public static readonly string MethodParameterType = typeof(ExampleStageHandler).FullName + "." + nameof(ScriptContext);

    /// <summary> /// Тип значения, возвращаемого методом <see cref="MethodName"/> . /// </summary> public static readonly string MethodReturnType = nameof(Boolean);

    #endregion

    #region Fields

    private readonly IKrCompilationCache krCompilationCache; private readonly IUnityContainer unityContainer;

    #endregion

    #region Constructors

    public ExampleStageHandler( IKrCompilationCache krCompilationCache, IUnityContainer unityContainer) { this.krCompilationCache = krCompilationCache; this.unityContainer = unityContainer; }

    #endregion

    #region Base Overrides

    /// <inheritdoc/> public override async Task<StageHandlerResult> HandleStageStartAsync(IStageTypeHandlerContext context) { var stage = context.Stage; var digest = stage.SettingsStorage.TryGet<string>(EduConstants.KrExampleStageSettingsVirtual.Digest); var scriptContext = new ScriptContext(digest); var result = await this.RunScriptAsync(context, scriptContext);

    context.ValidationResult.AddInfo(this, $"Результат выполнения сценария: {result}");

    return StageHandlerResult.CompleteResult; }

    #endregion

    #region Private Methods

    /// <summary> /// Выполняет дополнительный сценарий этапа (<see cref="MethodName"/>). /// </summary> /// <param name="context">Контекст обработчика этапа.</param> /// <param name="scriptContext">Параметр метода.</param> /// <returns>Результат выполнения сценария.</returns> public async Task<bool> RunScriptAsync( IStageTypeHandlerContext context, ScriptContext scriptContext) { var inst = await HandlerHelper.CreateScriptInstanceAsync( this.krCompilationCache, context.Stage.ID, context.ValidationResult, context.CancellationToken);

    await HandlerHelper.InitScriptContextAsync(this.unityContainer, inst, context);

    // Если дополнительный метод не имеет возвращаемого значения, то следует использовать метод IKrScript.InvokeExtraAsync(string, obecjt, bool). return await inst.InvokeExtraAsync<bool>(MethodName, scriptContext); }

    #endregion } }

  6. Регистрация обработчика типа этапа.

    using Tessa.Extensions.Default.Server.Workflow.KrProcess.Workflow; using Tessa.Extensions.Shared.Workflow.KrProcess.Workflow; using Tessa.Platform; using Unity; using Unity.Lifetime;

    namespace Tessa.Extensions.Server.Workflow.KrProcess.Workflow.Handlers { /// <summary> /// Регистратор обработчиков типов этапов. /// </summary> [Registrator] public sealed class Registrator : RegistratorBase { /// <inheritdoc/> public override void RegisterUnity() { this.UnityContainer .RegisterType<ExampleStageHandler>(new ContainerControlledLifetimeManager()); }

    /// <inheritdoc/> public override void FinalizeRegistration() { this.UnityContainer .TryResolve<IKrProcessContainer>() ? .RegisterHandler<ExampleStageHandler>(CustomStageTypeDescriptors.ExampleStageDescriptor); } } }

  7. Создание расширения на сериализацию параметров этапов, которое будет выполнять получение и сохранение информации о дополнительных методах этапов.

    using System; using System.Threading.Tasks; using Tessa.Extensions.Default.Server.Workflow.KrCompilers; using Tessa.Extensions.Default.Server.Workflow.KrProcess.Serialization; using Tessa.Extensions.Default.Shared.Workflow.KrProcess; using Tessa.Extensions.Server.Workflow.KrProcess.Workflow.Handlers; using Tessa.Extensions.Shared.Workflow.KrProcess; using Tessa.Extensions.Shared.Workflow.KrProcess.Workflow; using Tessa.Platform.Storage;

    namespace Tessa.Extensions.Server.Workflow.KrProcess.Serialization { /// <summary> /// Расширения на сериализацию параметров этапов. Выполняет получение и сохранение информации о дополнительных методах этапов. /// </summary> public sealed class CustomExtraSourcesStageRowExtension : ExtraSourcesStageRowExtensionBase { #region Constructors

    public CustomExtraSourcesStageRowExtension( IExtraSourceSerializer extraSourceSerializer) : base(extraSourceSerializer) { }

    #endregion

    #region Base Overrides

    /// <inheritdoc /> public override Task BeforeSerialization( IKrStageRowExtensionContext context) { var rows = context.InnerCard.GetStagesSection().Rows;

    foreach (var row in rows) { var stageTypeID = row.TryGet<Guid?>(KrConstants.KrStages.StageTypeID);

    if (stageTypeID == CustomStageTypeDescriptors.ExampleStageDescriptor.ID) { var extraSources = this.GetExtraSources(row);

    MoveSourceFromStageSettingsToExtraSources( extraSources, context, row, EduConstants.KrExampleStageSettingsVirtual.Script, ExampleStageHandler.MethodDisplayName, ExampleStageHandler.MethodName, ExampleStageHandler.MethodParameterType, ExampleStageHandler.MethodParameterName);

    this.SetExtraSources(row, extraSources); } }

    return Task.CompletedTask; }

    /// <inheritdoc /> public override Task DeserializationBeforeRepair( IKrStageRowExtensionContext context) { var rows = context.CardToRepair.Sections[KrConstants.KrStages.Virtual].Rows;

    foreach (var row in rows) { var stageTypeID = row.TryGet<Guid?>(KrConstants.KrStages.StageTypeID);

    if (stageTypeID == CustomStageTypeDescriptors.ExampleStageDescriptor.ID) { var extraSources = this.GetExtraSources(row); MoveSourceFromExtraSourcesToStageSettings( extraSources, row, EduConstants.KrExampleStageSettingsVirtual.Script); } }

    return Task.CompletedTask; }

    #endregion } }

  8. Регистрация расширения CustomExtraSourcesStageRowExtension.

    using Tessa.Extensions.Default.Server.Workflow.KrProcess.Serialization; using Unity; using Unity.Lifetime;

    namespace Tessa.Extensions.Server.Workflow.KrProcess.Serialization { [Registrator] public sealed class Registrator : RegistratorBase { /// <inheritdoc/> public override void RegisterUnity() { this.UnityContainer .RegisterType<CustomExtraSourcesStageRowExtension>(new ContainerControlledLifetimeManager()); }

    /// <inheritdoc/> public override void RegisterExtensions(IExtensionContainer extensionContainer) { extensionContainer .RegisterExtension<IKrStageRowExtension, CustomExtraSourcesStageRowExtension>(x => x .WithOrder(ExtensionStage.AfterPlatform) .WithUnity(this.UnityContainer) .WhenRouteCardTypes(RouteCardType.Template)) ; } } }

Форматтер типа этапа

В таблице с этапами есть столбцы “Срок”, “Участники” и “Настройки”

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

Для написания собственного форматтера необходимо создать класс, наследующий StageTypeFormatterBase и переопределить два метода:

  • FormatClient(context) - форматирование ячеек на клиенте. Выполняется при открытии карточки и при каждом закрытии редактора строки. В контексте доступны настройки этапа в виде виртуальных секций.

  • FormatServer(context) - форматирование ячеек на сервере. Выполняется при сохранении карточки. В контексте доступны настройки этапа в виде хранилища ключ-значение. Серверное форматирование может быть полезно для отображения этапов в представлениях и в легком клиенте.

Рассмотрим пример форматтера для этапа смены состояния:

public sealed class ChangeStateStageTypeFormatter : StageTypeFormatterBase { private const string KrChangeStateSettingsVirtual = nameof(KrChangeStateSettingsVirtual); private static readonly string SettingsStateName = StageTypeSettingsNaming.PlainColumnName(KrChangeStateSettingsVirtual, "StateName");

public override Task FormatClient(IStageTypeFormatterContext context) { // На клиенте доступны виртуальные секции. var state = context.StageRow.Fields.TryGet<string>(SettingsStateName); SetState(state, context);

return Task.CompletedTask; }

public override Task FormatServer( IStageTypeFormatterContext context) { // На сервере доступен словарь с настройками. var state = context.Settings.TryGet<string>(SettingsStateName); SetState(state, context);

return Task.CompletedTask; }

private static void SetState( string state, IStageTypeFormatterContext context) { context.DisplaySettings = string.IsNullOrEmpty(state) ? string.Empty : state[0] == '$' ? string.Concat("{$UI_KrChangeState_State}: ", "{", state, "}") : string.Concat("{$UI_KrChangeState_State}: ", state); } }

При регистрации необходимо связать форматтер с дескриптором. Форматтер и регистратор должны располагаться в Shared расширениях, т.к. форматирование происходит как на клиенте, так и на сервере.

[Registrator(Tag = RegistratorTag.Default)] public class StageTypeFormattersRegistrator : RegistratorBase { public override void RegisterUnity() { this.UnityContainer .RegisterType<ChangeStateStageTypeFormatter>(new ContainerControlledLifetimeManager()) ; }

public override void FinalizeRegistration() { this.UnityContainer .Resolve<IStageTypeFormatterContainer>() .RegisterFormatter<ChangeStateStageTypeFormatter>(StageTypeDescriptors.ChangesStateDescriptor); } }

UI обработчик типа этапа

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

Предусмотрены три метода:

  • Initialize - выполняется при открытии окна редактирования этапа. В данном методе можно проинициализировать элементы управления, выполнить подписки на события.

  • Finalize - выполняется при закрытии окна редактирования этапа. В данном методе необходимо выполнить отписки от событий.

  • Validate - выполняется перед закрытием окна редактирования этапа. В контексте доступен ValidationResult, который можно при необходимости заполнить.

В качестве примера рассмотрим UI обработчик этапа управления процессом.

public sealed class ProcessManagementUIHandler : StageTypeUIHandlerBase { private static readonly object stageMode = 0; private static readonly object groupMode = 1; private static readonly object signalMode = 5;

private const string StageGroupAlias = "StageGroup"; private const string StageRowAlias = "StageRow"; private const string SignalAlias = "Signal";

private DefaultFormSimpleViewModel form; private IControlViewModel stageControl; private IControlViewModel groupControl; private IControlViewModel signalControl; private bool initialized = false;

public override Task Initialize( IKrStageTypeUIHandlerContext context) { // Достаем все необходимые элементы управления this.form = context.RowModel.MainForm as DefaultFormSimpleViewModel; if (this.form is null || !this.form.Controls.TryGet(StageRowAlias, out this.stageControl) || !this.form.Controls.TryGet(StageGroupAlias, out this.groupControl) || !this.form.Controls.TryGet(SignalAlias, out this.signalControl)) { return Task.CompletedTask; }

if (this.stageControl is null || this.groupControl is null || this.signalControl is null) { return Task.CompletedTask; }

this.initialized = true;

// Обновляем видимость элементов управления this.UpdateVisibility(context.Row[KrConstants.KrProcessManagementStageSettingsVirtual.ModeID]); // Подписываемся на события изменения режима context.Row.FieldChanged += this.ModeChanged; }

public override Task Finalize( IKrStageTypeUIHandlerContext context) { if (this.initialized) { // Отписываемся от события изменения режима context.Row.FieldChanged -= this.ModeChanged; }

this.initialized = false;

return Task.CompletedTask; }

public override Task Validate(IKrStageTypeUIHandlerContext context) { // Проверяем, что режим указан if (context.Row.TryGet<int?>(KrConstants.KrProcessManagementStageSettingsVirtual.ModeID) is null) { // Если режим не указан, то показываем ошибку context.ValidationResult.AddError(this, "$KrStages_ProcessManagement_ModeNotSpecified"); }

return Task.CompletedTask; }

private void ModeChanged( object s, CardFieldChangedEventArgs args) { if (args.FieldName != KrConstants.KrProcessManagementStageSettingsVirtual.ModeID) { return; }

this.UpdateVisibility(args.FieldValue); }

private void UpdateVisibility( object value) { this.stageControl.ControlVisibility = Visibility.Collapsed; this.groupControl.ControlVisibility = Visibility.Collapsed; this.signalControl.ControlVisibility = Visibility.Collapsed;

if (Equals(value, stageMode)) { this.stageControl.ControlVisibility = Visibility.Visible; } else if (Equals(value, groupMode)) { this.groupControl.ControlVisibility = Visibility.Visible; } else if (Equals(value, signalMode)) { this.signalControl.ControlVisibility = Visibility.Visible; } this.form.Rearrange(); } }

Регистрация обработчика представлена ниже:

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

public override void FinalizeRegistration() { this.UnityContainer .Resolve<IKrProcessUIContainer>() .RegisterUIHandler<ProcessManagementUIHandler>(StageTypeDescriptors.ProcessManagementDescriptor) ; } }

События

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

Подписка на события

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

  • HandleEvent(context) - метод, вызываемый при возникновении события. В контексте доступна информация о полном состоянии процесса в момент возникновения события.

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

В качестве примера рассмотрим подписку на событие регистрации.

public class RegistrationEventExtension: KrEventExtension { public override Task HandleEvent( IKrEventExtensionContext context) { // .... } }

Регистрация подписчика выглядит следующим образом:

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

public override void RegisterExtensions(IExtensionContainer extensionContainer) { extensionContainer .RegisterExtension<IKrEventExtension, RegistrationEventExtension>(x => x .WithOrder(ExtensionStage.AfterPlatform) .WithUnity(this.UnityContainer) .WhenEventType(DefaultEventTypes.RegistrationEvent)) ; }

}

Генерация событий

Сгенерировать событие очень просто. Достаточно получить через Unity объект, реализующий интерфейс IKrEventManager. Это можно сделать через конструктор или напрямую. Далее достаточно вызвать метод RaiseAsync, в который передать текущий контекст.

await this.eventManager.RaiseAsync(context);

Клиентские команды

Процесс выполняется полностью на сервере. Однако для достижения интерактивности и повышения опыта взаимодействия (UX) необходима возможность совершать действия на клиентской стороне приложения. Для этого применяется механизм клиентских команд. В процессе обработки запроса в подсистеме маршрутов создается массив клиентских команд, который заполняется по мере работы процесса. В конце набор клиентских команд возвращается клиентскому приложению, где интерпретируется на основе типа команды.

Команда состоит из типа команды (строка) и набора параметров, представлемых в виде хранилища ключ-значение.

Добавление команды

Добавление команды производится с помощью метода TryAddClientCommand зависимости IKrScope. Все собранные за несколько запросов команды будут объединены в один массив и отправлены на клиент.

Пример использования клиентских команд

this.krScope.TryAddClientCommand( new KrProcessClientCommand( <Имя клиентской команды>, new Dictionary<string, object>() // Дополнительные параметры. { [<Имя параметра>] = <Значение параметра> } ));

В типовом решении предусмотрены следующие стандартные клиентские команды (Tessa.Extensions.Default.Shared.Workflow.KrProcess.ClientCommandInterpreter.DefaultCommandTypes):

  • OpenCard - открывает существующую карточку.

    Параметры

    Параметр
    Тип значения Описание
    KrConstants.Keys.NewCardID Guid? Идентификатор открываемой карточки или значение null, если идентификатор задаётся
    другим способом.
    KrConstants.Keys.TypeID Guid? Идентификатор типа отрываемой карточки или значение null, если идентификатор типа неизвестен.
    KrConstants.Keys.TypeName string Имя типа отрываемой карточки или значение null, если имя типа неизвестно.
  • CreateCardViaTemplate - создаёт новую карточку по шаблону на клиенте (карточка не будет сохранена, у пользователя должны быть права на сохранение карточки).

    Параметры

    Параметр
    Тип значения Описание
    KrConstants.Keys.TemplateID Guid Идентификатор шаблона, по которому создаётся карточка.
    KrConstants.Keys.NewCard byte[] Дополнительный параметр. Сериализованная заготовка карточки используемая для заполнения создаваемой по шаблону.
    KrConstants.Keys.NewCardSignature byte[] Дополнительный параметр. Используется совместно с KrConstants.Keys.NewCard. Сериализованная подпись заготовки карточки.
  • CreateCardViaDocType - создаёт новую карточку заданного типа.

    Параметры

    Параметр
    Тип значения Описание
    KrConstants.Keys.TypeID Guid Идентификатор типа создаваемой карточки.
    KrConstants.Keys.NewCard byte[] Дополнительный параметр. Сериализованная заготовка карточки используемая для заполнения создаваемой по шаблону.
    KrConstants.Keys.NewCardSignature byte[] Дополнительный параметр. Используется совместно с KrConstants.Keys.NewCard. Сериализованная подпись заготовки карточки.
    KrConstants.Keys.DocTypeID Guid Дополнительный параметр. Идентификатор типа документа создаваемой карточки.
    KrConstants.Keys.DocTypeTitle string Дополнительный параметр. Используется совместно с <see cref=”KrConstants.Keys.DocTypeID”/>. Имя типа документа создаваемой карточки.
  • RefreshAndNotify - обновляет список активных заданий пользователя и показывает уведомление при необходимости.

  • ShowConfirmationDialog - отображает заданное сообщение как результат валидации типа ValidationResultType.Info.

    Параметры

    Параметр
    Тип значения Описание
    text string Текст сообщения. Строка не должна быть пустой, состоять из одних пробелов или иметь значение null.
  • ShowAdvancedDialog - Открывает карточку в диалоге. Используется при обработке запроса из подсистемы маршрутов.

    Параметры

    Параметр
    Тип значения Описание
    KrConstants.Keys.ProcessInstance Хранилище объекта типа KrProcessInstance Информация о процессе маршрута.
    KrConstants.Keys.CompletionOptionSettings Хранилище объекта типа CardTaskCompletionOptionSettings Параметры диалога.
  • WeShowAdvancedDialog - Открывает карточку в диалоге. Используется при обработке запроса из Workflow Engine.

    Параметры

    Параметр
    Тип значения Описание
    KrConstants.Keys.CompletionOptionSettings Запрос на обработку процесса WorkflowEngine и его подпись. Для получения следует использовать метод WorkflowEngineExtensions.SetProcessInfo( Dictionary{string, object}, Guid, string, Guid?). Для получения WorkflowEngineExtensions.GetProcessRequest( IDictionary{string, object}). Info запроса на обработку процесса WorkflowEngine с его подписью.
    KrConstants.Keys.CompletionOptionSettings Хранилище объекта типа CardTaskCompletionOptionSettings сохранённое по ключу WorkflowDialogAction.DialogSettingsKey в коллекции ключ-значение получаемое для данного параметра Параметры диалога.

Создание собственных клиентских команд

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

В команде указываются следующие свойства:

  • CommandType - идентификатор типа команды. По данному значению осуществляется привязка обработчика команды.

  • Parameters - хранилище ключ-значение, заполняемое параметрами команды. Каждая команда определяет свой набор параметров.

В качестве примера рассмотрим команду обновления списка текущих задач и отображение уведомления

public sealed class RefreshAndNotifyClientCommandHandler : ClientCommandHandlerBase { private readonly IKrNotificationManager notificationManager;

public RefreshAndNotifyClientCommandHandler(IKrNotificationManager notificationManager) { // В клиентских командах можно получать любые IoC-зависимости this.notificationManager = notificationManager; }

/// <inheritdoc/> public override async Task Handle( IClientCommandHandlerContext context) { if (await this.notificationManager.CanCheckTasksAsync(context.CancellationToken)) { var _ = this.notificationManager.CheckTasksAsync(cancellationToken: context.CancellationToken); } } }

Регистрация клиентской команды выглядит следующим образом:

[Registrator] public sealed class Registrator : RegistratorBase { public override void RegisterUnity() { this.UnityContainer .RegisterType<IClientCommandHandler, RefreshAndNotifyClientCommandHandler>(new ContainerControlledLifetimeManager()) ; }

public override void FinalizeRegistration() { this.UnityContainer .Resolve<IClientCommandInterpreter>() .RegisterHandler<RefreshAndNotifyClientCommandHandler>(DefaultCommandTypes.RefreshAndNotify) ; } }

Глобальные сигналы

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

Только основной процесс (KrProcess) поддерживает приём сигналов без указания дополнительной информации для определения получателя сигнала. Для отправки сигнала вторичному процессу необходимо указать: его идентификатор в параметре processID и тип KrConstants.KrSecondaryProcessName, в параметре processTypeName.

Стандартные глобальные сигналы

В типовом решении предусмотрены следующие сигналы для управления процессом:

  • KrStartProcessSignal – запускает основной процесс. Может быть отправлен только основному процессу. Опционально могут быть указаны параметры запускаемого процесса, в том числе параметр KrConstants.Keys.NotMessageHasNoActiveStages позволяющий отключить сообщение при отсутствии этапов доступных для выполнения.

    card .GetWorkflowQueue() .AddSignal( KrConstants.KrProcessName, KrConstants.KrStartProcessSignal, parameters: new Dictionary<string, object>(StringComparer.Ordinal) { { KrConstants.Keys.NotMessageHasNoActiveStages, BooleanBoxes.True } });

  • KrStartProcessUnlessStartedGlobalSignal – запускает основной процесс, если он не запущен, иначе не выполняет никаких действий. Может быть отправлен только основному процессу. Опционально могут быть указаны параметры запускаемого процесса, в том числе параметр KrConstants.Keys.NotMessageHasNoActiveStages позволяющий отключить сообщение при отсутствии этапов доступных для выполнения.

    card .GetWorkflowQueue() .AddSignal( KrConstants.KrProcessName, KrConstants.KrStartProcessUnlessStartedGlobalSignal, parameters: new Dictionary<string, object>(StringComparer.Ordinal) { { KrConstants.Keys.NotMessageHasNoActiveStages, BooleanBoxes.True } });

  • KrCancelProcessGlobalSignal – отмена процесса с переводом всех этапов в состояние “Не запущен”.

    card .GetWorkflowQueue() .AddSignal(KrConstants.KrProcessName, KrConstants.KrCancelProcessGlobalSignal);

  • KrSkipProcessGlobalSignal – пропуск процесса с переводом всех незапущенных этапов в состояние “Пропущен”.

    card .GetWorkflowQueue() .AddSignal(KrConstants.KrProcessName, KrConstants.KrSkipProcessGlobalSignal);

  • KrTransitionGlobalSignal – переход внутри процесса.

    Общие параметры:

    • KrConstants.KrTransitionKeepStates – значение true, если должно быть сохранено текущее состояние этапов при выполнении перехода.

    Режимы работы:

    • Переход на этап

      card .GetWorkflowQueue() .AddSignal( KrConstants.KrProcessName, KrConstants.KrTransitionGlobalSignal, parameters: new Dictionary<string, object> { [KrConstants.StageRowID] = stageRowID, });

    • Переход в начало группы

      card .GetWorkflowQueue() .AddSignal( KrConstants.KrProcessName, KrConstants.KrTransitionGlobalSignal, parameters: new Dictionary<string, object> { [KrConstants.StageGroupID] = stageGroupID, });

    • Переход в начало текущей группы

      card .GetWorkflowQueue() .AddSignal( KrConstants.KrProcessName, KrConstants.KrTransitionGlobalSignal, parameters: new Dictionary<string, object> { [KrConstants.KrTransitionCurrentGroup] = BooleanBoxes.True, });

    • Переход на следующую группу (если следующая группа отсутствует, процесс будет пропущен)

      card .GetWorkflowQueue() .AddSignal( KrConstants.KrProcessName, KrConstants.KrTransitionGlobalSignal, parameters: new Dictionary<string, object> { [KrConstants.KrTransitionNextGroup] = BooleanBoxes.True, });

    • Переход на предыдущую группу

      card .GetWorkflowQueue() .AddSignal( KrConstants.KrProcessName, KrConstants.KrTransitionGlobalSignal, parameters: new Dictionary<string, object> { [KrConstants.KrTransitionPrevGroup] = BooleanBoxes.True, });

Создание собственных сигналов

При необходимости реализовать собственное воздействие на процесс можно написать собственный обработчик глобального сигнала определенного типа. Для этого необходимо написать класс, наследующий GlobalSignalHandlerBase и реализовать метод Handle(context). В контексте доступен активный этап на текущий момент, а также полный контекст подсистемы маршрутов, позволяющий в обход стандартной логики изменять текущий этап.

При работе с контекстом подсистемы маршрутов следует быть аккуратным, т.к. такое изменение оказывает влияние на весь процесс, а не на конкретный этап.

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

public sealed class CancelProcessSignalHandler : GlobalSignalHandlerBase { private readonly IKrStageInterrupter interrupter;

public CancelProcessSignalHandler( IKrStageInterrupter interrupter) { this.interrupter = interrupter; }

/// <inheritdoc /> public override async Task Handle( IGlobalSignalHandlerContext context) { // Текущий этап присутствует в контексте var currentStage = context.Stage;

// Выполняем прерывание этапа var completelyInterrupted = await this.interrupter.InterruptStageAsync(new KrStageInterrupterContext( context.Stage, context.RunnerContext, context.WorkflowAPI, context.RunnerMode, ci => ci ? KrProcessState.Default : new KrProcessState(KrConstants.CancelellationProcessState)));

// Этап полностью прерван, выполнений вложенных сохранений не требуется. if (completelyInterrupted) { context.RunnerContext.CurrentApprovalStageRowID = null; KrProcessHelper.SetInactiveStateToAllStages(context.RunnerContext.WorkflowProcess.Stages); } } }

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

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

public override void FinalizeRegistration() { this.UnityContainer .Resolve<IKrProcessContainer>() .RegisterGlobalSignal<CancelProcessSignalHandler>(KrConstants.KrCancelProcessGlobalSignal) ;

} }

Фильтрация обработчиков сигналов

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

Фильтр по типу сигнала

[Registrator] public sealed class Registrator : RegistratorBase { public override void FinalizeRegistration() { this.UnityContainer .Resolve<IKrProcessContainer>() .AddFilter(SignalFilter.Exclude(KrConstants.KrCancelProcessGlobalSignal)) ; } }

Фильтр по паре (тип сигнала; обработчик сигнала)

[Registrator] public sealed class Registrator : RegistratorBase { public override void FinalizeRegistration() { this.UnityContainer .Resolve<IKrProcessContainer>() // Добавляем фильтр на типовой обработчик сигнала .AddFilter( SignalHandlerFilter.Exclude( new SignalFilterItem( KrConstants.KrCancelProcessGlobalSignal, typeof(CancelProcessSignalHandler))))

// Добавляем свой тип сигнала .RegisterGlobalSignal<CustomCancelProcessSignalHandler>(KrConstants.KrCancelProcessGlobalSignal) ; } }

Специальные типы карточек, используемые в подсистеме маршрутов

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

    Warning

    Значения следующих полей не переносятся при сохранении карточки документа в основной сателлит:

    • KrApprovalCommonInfoVirtual.ApprovedBy – строка содержащая список согласовавших/подписавших документ.

    • KrApprovalCommonInfoVirtual.CurrentApprovalStageRowID – идентификатор текущего этапа.

    • KrApprovalCommonInfoVirtual.DisapprovedBy – строка содержащая список не согласовавших/отказавших в подписании документ.

    • KrApprovalCommonInfoVirtual.MainCardID – идентификатор основной карточки.

    • KrApprovalCommonInfoVirtual.StateChangedDateTimeUTC – дата и время изменения состояния карточки.

    • KrApprovalCommonInfoVirtual.StateID – идентификатор состояния карточки.

    • KrApprovalCommonInfoVirtual.StateName – название состояния карточки.

    Для их изменения необходимо обратиться к одноимённым полям секции KrApprovalCommonInfo основного сателлита. Пример получения карточки основного сателлита и изменения значений его полей приведён в руководстве разработчика в п. Изменение состояния карточки, которое было добавлено в таблицу-перечисление KrDocState.

  • KrSatellite – карточка-сателлит, содержащая информацию по основному процессу.

  • KrSecondarySatellite – карточка-сателлит, содержащая информацию по вторичному процессу.

Back to top