Продвинутые примеры настройки процессов¶
Пример подготовки, согласования и исполнения служебной записки/заявки¶
Рассмотрим пример реализации процесса в Workflow Engine описанного в примере “Подготовка, согласование и исполнение служебной записки/заявки” в Руководстве администратора, реализованного с использованием средств подсистемы маршрутов.
Ниже представлена общая схема процесса.

Описание процесса¶
Пользователь инициирует служебную записку (СЗ) разных типов: обычную и финансовую. Пользователь не создает никаких карточек сам, вместо этого он инициирует процесс нужного типа, и уже система решает, что ему нужно для продолжения.
В зависимости от выбранного пользователем типа процесса система создает карточку заявки по различным шаблонам, отправляет по ней инициатору задание на заполнение заявки, при этом процесс организован так, что карточка заявки сама собой открывается пользователю в окне диалога.
Далее инициатор заполняет карточку заявки и отправляет служебную записку на согласование. При отправке на согласование система выполняет проверку того, указан ли тип заявки, если он не указан, то пользователь не сможет отправить заявку далее по процессу.
Если заявка является финансовой:
-
После отправки на согласование будет предложено предоставить финансовое обоснование или ввести комментарий.
-
Когда заявка инициирована и приложено обоснование, она отправляется на согласование. В нем участвуют роли: “Руководитель инициатора”, “Финансовый департамент” и “Главный бухгалтер”. С каждым циклом согласования времени на согласование система будет давать всё меньше.
Далее заявка отправляется на исполнение. Исполняют ее различные роли в зависимости от типа заявки. После исполнения система отправляет задание инициатору с просьбой подтвердить выполнение.
Если инициатор не подтверждает выполнение заявки, то система вновь отправляет задание исполнителю заявки с комментарием недовольного инициатора.
Давайте создадим новую служебную записку. Нажмите тайл “Обычная СЗ” в группе тайлов “СЗ Workflow Engine” на правой панели.

Будет открыто диалоговое окно, содержащее сформированный по шаблону документ.

После отправки на согласование система стартует процесс и присылает задание на исполнение заявки, отправленное на роль “ДИТ”. При этом карточка автоматически открывается в клиентском рабочем месте.

Посмотрим, как выглядит процесс. Для этого откроем редактор экземпляра процесса Левое меню → Действия → Процессы → Подготовка, согласование и исполнение служебной записки/заявки. В настоящий момент активно действие “Исполнение обычной СЗ”.

Как видите, в задании уже написано, что нам нужно сделать.
Теперь нажмите кнопку “В работу” в задании.
Теперь вам доступны все возможности работы с этим заданием. Задание можно просто завершить. Еще вы можете отправить задание другому сотруднику или сотрудникам, вы можете создать подчиненное задание, чтобы получить какие-то дополнительные материалы и данные. Возможностей очень много, и они подробно описаны в руководстве пользователя в разделе “Работа с задачами”.
Давайте завершим задание, чтобы сообщить системе что заполнение заявки вами завершено и можно продолжить маршрут. Нажмите кнопку “Завершить” в задании. Далее на форме завершения задания нажмите кнопку “Завершить” еще раз для подтверждения. Комментарий можно оставить пустым.
Давайте создадим новую обычную служебную записку и очистим поле “Категория документа”. Выделите значение и удалите его, после чего отправьте документ на согласование.
Система создаст карточку, но процесс дальше нельзя продолжить до тех пор, пока не будет указана допустимая категория документа.

Давайте вернемся в карточку и заполним поле “Категория документа”. Выберите “Прочие СЗ” из выпадающего списка и опять отправьте на согласование.
И система немедленно присылает нам задание на исполнение заявки. Для удобства текущий сотрудник включен во все роли процесса, но в реальной жизни эти роли выполняют разные люди.
Состояние карточки изменилось на “На исполнении”. Часто со сменой состояния также настраивают изменения прав доступа на карточку, например, чтобы инициатор имел права на изменение данных и файлов карточки, только пока она в состоянии “Проект”.
Давайте возьмем задание в работу и завершим его. Система прислала инициатору (нам) задание на подтверждение выполнения заявки.
Возьмем задание в работу. Нажмите кнопку “В работу”. Обратите внимание, что это другое задание. В нем нам доступны только два варианта завершения “Выполнено” и “Не принято”. Эти варианты специально настроены для данного действия процесса.
Давайте не примем выполнение. Нажмите кнопку “Не принято”, заполните комментарий и еще раз нажмите “Не принято” для подтверждения завершения.

И мы опять переходим на этап выполнения заявки. Система вновь отправила задание исполнителю с комментарием инициатора.

Давайте опять возьмём задание в работу и затем завершим его. Нажмите “В работу”, затем нажмите “Завершить” и на форме завершения задания еще раз нажмите “Завершить”.
Система вновь присылает нам задание на подтверждение. Возьмите его в работу, и нажмите “Выполнено”.
Исполнение заявки принято, процесс завершен, карточка перешла в состояние “Исполнен”.

А теперь давайте инициируем финансовую заявку, чтобы посмотреть на работу согласования. Нажмите в правой панели тайл “Финансовая СЗ”.

Вновь система создала карточку заявки и открыла её в диалоговом окне. Система запустит процесс и отправит задание на роль “Инициатор”.

Задание уже взято в работу. Нажав на кнопку “Приложить обоснование” откроется диалоговое окно, где можно добавить файл или указать комментарий. Нажмите на панели инструментов кнопку “Отправить” для завершения задания.

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

Согласно схеме процесса на роль “Руководитель инициатора” приходит задание на согласование заявки. Срок задания составляет 5 рабочих дней на первом цикле согласования. Он будет уменьшаться на 1 день на каждом последующем цикле согласования вплоть до 1 дня.
Возьмите задание в работу. Это задание этапа согласования и оно предоставляет множество возможностей. Вы можете его делегировать, можете запрашивать комментарии у различных сотрудников (например, уточнить что-то у инициатора), можете делать дочерние согласования. Это подробно описано в руководстве пользователя в разделе “Согласование документов”.
Давайте не согласуем заявку. Нажмите “В работу”, далее “Не согласовать”, введите комментарий и далее еще раз нажмите “Не согласовать” для подтверждения.
Система отправляет задание инициатору на доработку.
Задача инициатора скорректировать заявку в связи с замечаниями. Давайте сделаем вид, что мы это сделали и начнем сразу новый цикл согласования. Нажмите “В работу” и затем “Начать новый цикл”.
Система снова отправляет задание на предоставление обоснования, где Инициатор в диалоговом окне может приложить файл или указать комментарий.
Теперь давайте согласуем документ. Нажмите “В работу”, затем “Согласовать” и еще раз “Согласовать” в форме завершения. Система сразу же отправляет следующее задание согласования на роль “Финансовый департамент” (и это опять мы - для удобства).
Если завершить и это задание, согласовав документ, мы попадем на этап согласования финансового обоснования главным бухгалтером. Укажем результат проверки и согласуем финансовое обоснование.

Мы попадаем на уже знакомый нам этап исполнения заявки, только исполнителем будет другая роль, потому что категория заявки “Финансовая СЗ”.
Дальнейший процесс мы подробно проходили в первой части этого примера. Обратите внимание, сколько мелких нюансов реализовано в этом процессе и при этом он остался чрезвычайно простым в настройке.
Скачать пример настроенного данного процесса можно по ссылке.
Создание процесса¶
Рассмотрим процесс создания описанного процесса.
Создайте новый шаблон бизнес процесса и настройте его следующим образом:
-
Название процесса, например, “Подготовка, согласование и исполнение служебной записки”;
-
Группа процесса, например, “Примеры”;
-
Снимите флаг Запуск из карточки;
-
Состояние флага Можно запускать несколько экземпляров оставьте без изменений;
-
Укажите тип карточки “Договорной документ”;
-
Добавьте кнопку бизнес-процесса “Обычная СЗ” и настройте её как показано на рисунке:

-
Добавьте кнопку бизнес-процесса “Финансовая СЗ” и настройте её как показано на рисунке:

В итоге должно получиться:

Откройте шаблон бизнес-процесса и разместите действия и связи как на следующем рисунке.

Все узлы содержат по одному действию. Соответствие наименования узлов и действий приведено в таблице.
Заголовок узла |
Действие |
|---|---|
| Старт процесса по финансовой СЗ, Старт процесса по обычной СЗ | Старт процесса |
| Инициализация финансовой СЗ, Инициализация обычной СЗ, Предоставление обоснования, Согласование обоснования | Диалог |
| Инициализация маршрута | Инициализация маршрута |
| Управление историей | Управление историей |
| Категория заявки, Категория заявки | Условие |
| Изменение категории СЗ | Задание |
| Согласование с руководителем и фин. департаментом | Согласование |
| Доработка | Доработка |
| На исполнении, Исполнен | Смена состояния |
| Исполнение обычной СЗ, Исполнение финансовой СЗ | Выполнение задачи |
| Подтвердить выполнение | Настраиваемое задание |
| Конец процесса | Конец процесса |
Для реализации возможности создания служебных записок двух разных категорий “Финансовая СЗ” и “Обычная СЗ” процесс содержит два действия “Старт процесса”, каждый из которых настроен на обработку своего сигнала. Так действие из узла “Старт процесса по финансовой СЗ” обрабатывает сигнал “StartFinancial”, а из “Старт процесса по обычной СЗ” - “StartCommon”.
Параметры действия из узла “Инициализация финансовой СЗ”:

Параметры действия из узла “Инициализация обычной СЗ”:

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

Параметры диалога, инициализирующего обычную СЗ:

Обратите внимание на установленный флаг Без отправки задания. Он позволяет использовать диалог в глобальных процессах, т.е. без использования карточки.
Кнопка “Отправить”, у обоих диалогов, настроена идентично:

В сценарии необходимо указать вызов метода StoreAndOpenCardAsync(WorkflowDialogContext) выполняющего сохранение и последующее открытие инициализированной карточки служебной записки:
await this.StoreAndOpenCardAsync(dialog);
Сам метод расположен в основном скрипте процесса (см. Редактор процесса):
#using System.Threading.Tasks;
#using Tessa.Cards;
#using Tessa.Extensions.Default.Server.Workflow.KrProcess;
#using Tessa.Extensions.Default.Shared.Workflow.KrProcess;
#using Tessa.Extensions.Default.Shared.Workflow.KrProcess.ClientCommandInterpreter;
/// <summary>
/// Асинхронно сохраняет и открывает на клиенте инициализируемую карточку служебной записки.
/// </summary>
/// <param name="dialog">Контекст скриптов обработки диалогов в Workflow Engine.</param>
/// <returns>Асинхронная задача.</returns>
protected async Task StoreAndOpenCardAsync(WorkflowDialogContext dialog)
{
var fileManager = this.Context.Container.Resolve<ICardFileManager>();
var tokenProvider = this.Context.Container.Resolve<IKrTokenProvider>();
var dialogCard = await dialog.GetCardObjectAsync();
// Обработка файлов приложенных к инициализируемой карточке служебной записки.
await using (var container = await fileManager.CreateContainerAsync(dialogCard, cancellationToken: this.Context.CancellationToken))
{
// Подготовка контента приложенных и/или изменённых файлов для последующего сохранения.
foreach(var dFile in container.FileContainer.Files)
{
if(!dFile.Content.HasData)
{
await dFile.ReplaceAsync(await dialog.GetFileContentAsync(dialogCard.Files.Single(i => i.RowID == dFile.ID)));
}
}
var storeResponse = await container.StoreAsync((c, request, ct) =>
{
tokenProvider.CreateToken(request.Card.ID).Set(request.Card.Info);
return new ValueTask();
},
cancellationToken: this.Context.CancellationToken);
this.ValidationResult.Add(storeResponse.ValidationResult);
if (!storeResponse.ValidationResult.IsSuccessful())
{
return;
}
}
// Добавление клиентской команды на открытие инициализированной карточки во вкладке web-клиента.
if (!(this.Context.ResponseInfo.TryGetValue(KrProcessSharedExtensions.KrProcessClientCommandInfoMark, out var commandsObj)
&& commandsObj is List<object> commands))
{
commands = new List<object>();
this.Context.ResponseInfo[KrProcessSharedExtensions.KrProcessClientCommandInfoMark] = commands;
}
commands.Add(
new KrProcessClientCommand(
DefaultCommandTypes.OpenCard,
new Dictionary<string, object>
{
[KrConstants.Keys.NewCardID] = dialogCard.ID
}).GetStorage());
// Установка инициализированной карточки служебной записки в качестве основной карточки процесса.
this.Context.SetMainCard(dialogCard.ID);
}
После инициализации карточки служебной записки начинается процесс её согласования.
Для действия “Инициализация процесса” не надо задавать никаких параметров, отличных от значений по умолчанию.
Все действия “Управление историей” в данном процессе имеют следующие параметры:

Параметры Группа истории заданий и Родительская группа привязаны к параметрам процесса (см. Форма для создания привязки) “Process.HistoryGroup” и “Process.ParentHistoryGroup”, соответственно, и имеют следующие значения: “Согласование - цикл” и “Согласование”.
Note
Обратите внимание:
-
Для правильной инициализации маршрута необходимо размещать действие “Инициализация маршрута” перед любыми действиями из группы “Маршруты”.
-
Для корректного формирования Истории заданий при переходе к новой итерации, аналогично используемой по умолчанию в подсистеме маршрутов, необходимо размещать действие “Управление историей” перед действием “Доработка”.
Для корректного формирования Истории заданий на первом цикле, надо перед любым действием, отправляющим задания, добавить действие “Управление историей”. Действие “Управление историей” достаточно добавить один раз в начале маршрута, что обеспечит создание группы истории заданий: Согласование → Согласование - цикл 1 (аналог этого действия в маршрутах - этап “Управление историей” из шаблона этапов “Новая итерация согласования”).
Более подробно про особенности действий из группы “Маршруты” см. в разделе Особенности действий.
Для управления выполнением процесса в зависимости от вида заявки используется действие “Условие”, расположенное в узле “Категория заявки”. Оно имеет три условия проверяющих значение категории заявки:
-
Категория заявки равна “Финансовая СЗ”.
Для проверки условия используется сценарий, проверяющий текущее значение идентификатора категории на равенство идентификатору категории “Финансовая СЗ”:
(await this.GetCardAsync()).DocumentCommonInfo.CategoryID == new Guid(0xd51477de, 0x8664, 0x473d, 0xab, 0x5f, 0xd1, 0xee, 0x0e, 0x60, 0x52, 0xc6)

-
Категория заявки равна “Прочие СЗ”.
Для проверки условия используется сценарий, проверяющий текущее значение идентификатора категории на равенство идентификатору категории “Прочие СЗ”:
(await this.GetCardAsync()).DocumentCommonInfo.CategoryID == new Guid(0x39779be7, 0x8715, 0x49af, 0x84, 0x37, 0x1b, 0x8b, 0x55, 0xd1, 0x1e, 0x24)

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

Изменение категории СЗ реализовано, через отправку задания типа “Доработка” из действия “Задача” расположенного в узле “Изменение категории СЗ”.

Перейдём к рассмотрению действий, отвечающих за процесс согласования финансовой СЗ внутри компании.
Предоставление финансового обоснования реализовано через диалог, расположенный в узле “Предоставление обоснования”, настроенный следующим образом:

Диалог имеет одну кнопку “Отправить” настроенную следующим образом:

В сценарии выполняется проверка задания комментария или приложения файлов:
const string justificationSectionAlias = "WfeJustification";
const string commentAlias = "Comment";
var dCard = await dialog.GetCardObjectAsync();
var hasFiles = dCard.TryGetFiles()?.Count > 0;
// Не указан комментарий в поле "Комментарий к обоснованию" карточки диалога и не приложен ни один файл?
if(string.IsNullOrEmpty(dCard.Sections[justificationSectionAlias].Fields.Get<string>(commentAlias))
&& !hasFiles)
{
this.ValidationResult.AddError(this, "Пожалуйста, приложите файл финансового обоснования или заполните комментарий.");
}
Перенос комментария к обоснованию и файлов в карточку служебной записки из карточки диалога выполняется в сценарии сохранения диалога:
#using System.Threading.Tasks;
#using Tessa.Cards;
const string justificationSectionAlias = "WfeJustification";
const string commentAlias = "Comment";
const string bSend = "Send";
const string mCardFileCategoryName = "Финансовое обоснование";
if(dialog.ButtonName == bSend)
{
// Перенос комментария из поля "Комментарий к обоснованию" карточки диалога в одноимённое поле карточки СЗ.
(await this.GetCardObjectAsync()).Sections.GetOrAdd(justificationSectionAlias).Fields[commentAlias] = (await dialog.GetCardObjectAsync()).Sections[justificationSectionAlias].Fields[commentAlias];
// Перенос файлов, приложенных к карточке диалога в карточку СЗ.
var mCardFileContainer = (await this.Context.GetFileContainerAsync(cancellationToken: this.Context.CancellationToken)).FileContainer;
var dCardFileContainer = (await dialog.GetFileContainerAsync()).FileContainer;
foreach(var dFile in dCardFileContainer.Files)
{
if(!dFile.Content.HasData)
{
var result = await dFile.EnsureContentDownloadedInUIAsync(cancellationToken: this.Context.CancellationToken);
this.ValidationResult.Add(result);
if(result.HasErrors)
{
return;
}
}
await mCardFileContainer
.BuildFile(dFile.Name)
.SetContent(dFile.Content)
.SetVersionToken(
(ft, ct) =>
{
var lastVersion = dFile.Versions.Last();
ft.Created = lastVersion.Created;
ft.CreatedByID = lastVersion.CreatedByID;
ft.CreatedByName = lastVersion.CreatedByName;
ft.Hash = lastVersion.Hash;
return new ValueTask();
})
.SetCategory(mCardFileCategoryName)
.AddWithNotificationAsync(cancellationToken: this.Context.CancellationToken);
}
}
После приложения финансового обоснования процесс переходит к его согласованию, реализуемого действием “Согласование”, расположенного в узле “Согласование с руководителем и фин. департаментом”. Параметры действия должны быть следующими:

Уменьшение длительности выполнения отправляемого задания реализовано через сценарий предобработки:
#using Tessa.Extensions.Default.Shared.Workflow.WorkflowEngine;
// В сценарии выполняется уменьшение длительности выполнения отправляемого задания в зависимости от числа циклов согласования.
var period = (await this.Context.GetAsync<double?>(WorkflowConstants.KrApprovalActionVirtual.SectionName, WorkflowConstants.KrApprovalActionVirtual.Period) ?? 1) - WorkflowHelper.GetProcessCycle(this.Context.ProcessInstance.Hash) + 1;
if(period < 1)
{
period = 1;
}
WorkflowEngineHelper.Set(this.Context.ActionInstance.Hash, period, WorkflowConstants.KrApprovalActionVirtual.SectionName, WorkflowConstants.KrApprovalActionVirtual.Period);
Для передачи информации о вернувшем на доработку и его комментарии, в сценарии варианта завершения “Не согласовать” задания типа “Согласование (KrApprove)”, выполняется сохранение этой информации в параметры процесса:
this.Process.SentToRebuild = this.Session.User.Name;
this.Process.RebuildComment = taskCard.KrTask.Comment;
После согласования с руководителем и финансовым департаментом, финансовое обоснование отправляется на согласование с главным бухгалтером. Оно реализовано с помощью действия “Диалог” из узла “Согласование обоснования”. Его параметры:

Диалог имеет две кнопки:
-
Согласовано

-
На доработку

Перенос значения комментария к обоснованию и файлов в карточку диалога выполняет сценарий инициализации диалога:
const string justificationSectionAlias = "WfeJustification";
const string commentAlias = "Comment";
const string fileCategoryName = "Финансовое обоснование";
var dCard = await dialog.GetCardObjectAsync();
var mCard = await this.GetCardObjectAsync();
dCard.ID = Guid.NewGuid();
// Перенос комментария к финансовому обоснованию из карточки СЗ в карточку диалога.
dCard.Sections[justificationSectionAlias].Fields[commentAlias] = mCard.Sections[justificationSectionAlias].Fields[commentAlias];
// Подготовка и копирование файлов из карточки СЗ в карточку диалога.
mCard.Files.RemoveAll(i => !string.Equals(i.CategoryCaption, fileCategoryName, StringComparison.Ordinal));
var result = await CardHelper.CopyFilesAsync(
mCard,
dCard,
this.Context.Container,
cancellationToken: this.Context.CancellationToken);
this.ValidationResult.Add(result);
if(result.HasErrors)
{
return;
}
var dCardFiles = dCard.Files;
// Сохранение информации о внешнем источнике контента файла.
var fileIDContentSources = new Dictionary<string, object>(dCardFiles.Count, StringComparer.Ordinal);
foreach(var dCardFile in dCardFiles)
{
fileIDContentSources.Add(dCardFile.RowID.ToString("N"), dCardFile.ExternalSource.FileID.ToString("N"));
}
this.Process.FileIDContentSources = fileIDContentSources;
Валидация полей выполняется в сценарии валидации диалога:
const string justificationSectionAlias = "WfeJustification";
const string verificationResultAlias = "VerificationResult";
const string bRebuildAlias = "Rebuild";
var verificationResult = (await dialog.GetCardObjectAsync()).Sections[justificationSectionAlias].Fields.Get<string>(verificationResultAlias);
if(dialog.ButtonName != null)
{
if(string.IsNullOrEmpty(verificationResult))
{
this.ValidationResult.AddError(this, "Заполните, пожалуйста, поле \"Результат проверки\".");
return;
}
if(string.Equals(dialog.ButtonName, bRebuildAlias, StringComparison.Ordinal))
{
this.Process.SentToRebuild = this.Session.User.Name;
this.Process.RebuildComment = verificationResult;
}
}
Для переноса файлов и комментария из поля “Результат проверки” карточки диалога в карточку СЗ используется сценарий сохранения диалога:
#using System.IO;
#using System.Threading.Tasks;
#using Tessa.Cards;
#using Tessa.Cards.Numbers;
const string bAgreedAlias = "Agreed";
const string bRebuildAlias = "Rebuild";
const string justificationSectionAlias = "WfeJustification";
const string verificationResultAlias = "VerificationResult";
const string mCardFileCategoryName = "Финансовое обоснование";
var mCard = await this.Context.GetMainCardAsync(cancellationToken: this.Context.CancellationToken);
var dCard = await dialog.GetCardObjectAsync();
var dCardFiles = dCard.TryGetFiles();
// В карточке диалога нет файлов?
if(dCardFiles?.Any() != true)
{
return;
}
if(string.Equals(dialog.ButtonName, bAgreedAlias, StringComparison.Ordinal)
|| string.Equals(dialog.ButtonName, bRebuildAlias, StringComparison.Ordinal))
{
Dictionary<Guid, Guid> fileIDContentSources;
if(this.Process.FileIDContentSources is Dictionary<string, object> fileIDContentSourcesObjDict)
{
fileIDContentSources = fileIDContentSourcesObjDict.ToDictionary(i => Guid.ParseExact(i.Key, "N"), j => Guid.ParseExact((string)j.Value, "N"));
}
else
{
fileIDContentSources = null;
}
Guid? GetFileIDContentSource(Guid fileRowID)
{
if(fileIDContentSources != null
&& fileIDContentSources.TryGetValue(fileRowID, out var fileIDContentSource))
{
return fileIDContentSource;
}
return default;
}
mCard.Sections[justificationSectionAlias].Fields[verificationResultAlias] = dCard.Sections[justificationSectionAlias].Fields[verificationResultAlias];
var mCardFileContainer = (await this.Context.GetFileContainerAsync(cancellationToken: this.Context.CancellationToken)).FileContainer;
var mFiles = mCardFileContainer.Files;
// Удаление файлов из основной карточки, удалённых из карточки диалога.
var deletedFileIDCollection = mFiles
.Where(mFile => string.Equals(mFile.Category?.Caption, mCardFileCategoryName, StringComparison.Ordinal))
.Select(mFile => mFile.ID)
.Except(dCardFiles
.Select(dCardFile => GetFileIDContentSource(dCardFile.RowID))
.Where(mFileID => mFileID.HasValue)
.Cast<Guid>()).ToArray();
for(var i = 0; i < deletedFileIDCollection.Length; i++)
{
await mFiles.RemoveWithNotificationAsync(mFiles.TryGet(deletedFileIDCollection[i]), cancellationToken: this.Context.CancellationToken);
}
// Перенос файлов из карточки диалога в основную карточку.
var dCardFileContainer = (await dialog.GetFileContainerAsync()).FileContainer;
ValidationResult result;
foreach(var dFile in dCardFileContainer.Files)
{
if(!dFile.Content.HasData)
{
result = await dFile.EnsureContentDownloadedInUIAsync(cancellationToken: this.Context.CancellationToken);
this.ValidationResult.Add(result);
if(result.HasErrors)
{
return;
}
}
var fileIDContentSource = GetFileIDContentSource(dFile.ID);
// Файл был перенесён из основной карточки?
if(fileIDContentSource.HasValue)
{
var mFile = mFiles.TryGet(fileIDContentSource.Value);
if(dFile.TryGetActualVersion().Number > 1)
{
result = await mFile.ReplaceAsync(await dFile.Content.GetAsync(this.Context.CancellationToken), this.Context.CancellationToken);
this.ValidationResult.Add(result);
if(result.HasErrors)
{
return;
}
}
result = await mFile.RenameAsync(dFile.Name, this.Context.CancellationToken);
this.ValidationResult.Add(result);
if(result.HasErrors)
{
return;
}
}
else
{
result = (await mCardFileContainer
.BuildFile(dFile.Name)
.SetContent(dFile.Content)
.SetVersionToken(
(ft, ct) =>
{
var lastVersion = dFile.Versions.Last();
ft.Created = lastVersion.Created;
ft.CreatedByID = lastVersion.CreatedByID;
ft.CreatedByName = lastVersion.CreatedByName;
ft.Hash = lastVersion.Hash;
return new ValueTask();
})
.SetCategory(mCardFileCategoryName)
.AddWithNotificationAsync(cancellationToken: this.Context.CancellationToken)).result;
ValidationResult.Add(result);
if(result.HasErrors)
{
return;
}
}
}
}
Доработка при согласовании внутри компании выполняется действием “Доработка”, расположенным в одноимённом узле:

Вывод имени и комментария вернувшего на доработку выполняется с помощью сценария предобработки:
#using Tessa.Extensions.Default.Shared.Workflow.WorkflowEngine;
var sentToRebuild = (string)this.Process.SentToRebuild;
if (!string.IsNullOrEmpty(sentToRebuild))
{
var digest = $"Служебная записка возвращена на доработку: {sentToRebuild}";
var comment = (string)this.Process.RebuildComment;
if(!string.IsNullOrWhiteSpace(comment))
{
digest += Environment.NewLine + Environment.NewLine + comment;
}
WorkflowEngineHelper.Set(
this.Context.ActionInstance.Hash,
digest,
WorkflowConstants.KrAmendingActionVirtual.SectionName,
WorkflowConstants.KrAmendingActionVirtual.Digest);
}
Перейдём к рассмотрению реализации процесса исполнения СЗ.
При переходе СЗ на исполнение у неё изменится состояние на “На исполнении” в действии “Смена состояния” узла “На исполнении”.
После чего процесс снова разветвляется в зависимости от категории СЗ в действии “Условие” узла “Категория заявки”. Условия в данном действии аналогичны ранее рассмотренным в действии “Условие” узла “Категория заявки”. Только отсутствует обработка варианта “Неизвестная категория СЗ”.
Если категория заявки “Финансовая СЗ”, то она отправляется на исполнение в узел “Исполнение финансовой СЗ” для обработки действием “Выполнение задачи”, иначе в узел “Исполнение обычной СЗ”.
Параметры действия из узла “Исполнение финансовой СЗ” указаны на рисунке:

Вывод комментария вернувшего СЗ на доработку выполняется с помощью сценария предобработки:
#using Tessa.Extensions.Default.Shared.Workflow.WorkflowEngine;
if (!string.IsNullOrEmpty((string)this.Process.RejectComment))
{
WorkflowEngineHelper.Set(
this.Context.ActionInstance.Hash,
"Инициатор заявки отклонил выполнение с комментарием:"
+ Environment.NewLine
+ this.Process.RejectComment,
WorkflowConstants.KrResolutionActionVirtual.SectionName,
WorkflowConstants.KrResolutionActionVirtual.Digest);
}
Действие, расположенное в узле “Исполнение обычной СЗ” имеет следующие параметры:

После исполнения СЗ у неё изменяется состояние на “Исполнен” в действии “Смена состояния” в одноимённом узле.
Параметры действия “Настраиваемое задание”, расположенного в узле “Подтвердить выполнение”, приведены на следующем рисунке:

Параметры настраиваемых вариантов завершения:
-
Выполнено.
Вариант завершения не имеет параметров отличных от значений по умолчанию.

-
Не принято.
Для возможности указания пользователем комментария в задании необходимо установить флаг Показать поле комментарий.
Проверка наличия комментария и его сохранение в параметрах процесса для отображения в задании при доработке выполняется с помощью скрипта:
var comment = (string)taskCard.KrTask.Comment;
if (string.IsNullOrWhiteSpace(comment)) { this.ValidationResult.AddError(this, "Пожалуйста, укажите комментарий."); return; }
this.Process.RejectComment = comment;
В итоге, после настройки параметров варианта завершения, должна получиться следующая картина:

Скачать настроенный процесс, описанный в данном разделе, можно по ссылке.
Настройка создания ссылок мобильного согласования в уведомлениях о задании¶
Для настройки создания ссылок мобильного согласования в уведомлениях о задании необходимо:
- Настроить работу мобильного согласования.
- Задать уведомление содержащее в тексте письма плейсхолдеры:
{optionsForMobileApproval}и{filesForMobileApproval}. Например, “Задание отправлено”. - Задать сценарий изменения уведомления:
#using Tessa.Cards.Caching #static Tessa.Extensions.Default.Shared.Notices.NotificationHelper;
ModifyTaskCaption( email, task);

Настройка создания записи информации о задании в листе согласования действиями не из группы “Маршруты”¶
Для формирования записи в листе согласования необходимо добавить методы в сценарии:
-
Сценарий инициализации задания:
#using Tessa.Extensions.Default.Server.Workflow.WorkflowEngine #using Tessa.Extensions.Default.Shared.Workflow.WorkflowEngine
await this.Context.AddActiveTaskAsync(task.RowID); await this.Context.AddToHistoryAsync(task.RowID, WorkflowHelper.GetProcessCycle(this.Context.ProcessInstance.Hash));
-
Сценарий постобработки действия (должен стоять флаг “Выполнять на любой сигнал”).
#using Tessa.Extensions.Default.Server.Workflow.WorkflowEngine
foreach (var task in this.Context.Tasks) { if (task.State == CardRowState.Deleted) { await this.Context.TryRemoveActiveTaskAsync(task.RowID); } }
Note
Методы расположены в классе Tessa.Extensions.Default.Server.Workflow.WorkflowEngine.WorkflowEngineContextExtension.
Создание действий¶
Система позволяет создавать свои типы действий и использовать их в бизнес-процессах.
Для того, чтобы сделать новый тип действия нужно реализовать следующий набор объектов и классов.
Дескриптор действия¶
В первую очередь нужно создать объект-дескриптор действия. Для этого нужно создать свой экземпляр объекта с типом WorkflowActionDescriptor. Данный объект содержит описание действия и связь с карточкой настроек из типов карточек. Данный объект можно создать как статическое поле какого-нибудь класса.
WorkflowActionDescriptor имеет следующий набор свойств:
-
ID - определяет уникальный идентификатор типа действия и является идентификатором типа карточки с настройками. Обязательный параметр для заполнения.
-
Icon - имя ресурса с иконкой для действия. Выбранная иконка отображается на боковой панели с действиями и как иконка по умолчанию для новых узлов при добавлении действия. По умолчанию имеет значение “Thin1”.
-
Group - определяет имя группы, в которую включено данное действие на боковой панели. По умолчанию имеет значение “$WorkflowEngine_Groups_Main” (Основные).
-
Order - определяет порядок действий на боковой панели. По умолчанию равен 100.
-
IsStandAlone - определяет, является ли данное действие одиночным (см. раздел Особенности действий). По умолчанию имеет значение false.
-
ProcessNodeExit - определяет, должно ли действие обрабатывать системный Exit-сигнал (см. раздел Особенности действий). По умолчанию имеет значение false.
-
Methods - массив дескрипторов компилируемых методов действия.
-
NotPersistentModeAllowed - определяет, что действие может выполняться в неперсистентном режиме обработки процесса. По умолчанию имеет значение false.
-
NormalizationSettings - настройки нормализации для настроек действия. Позволяют использовать платформенный механизм Справочников нормализации. Свойство имеет тип
WorkflowNormalizationSettingsи по умолчанию имеет значение null.Настройки нормализации содержат свойство
Settings. Это свойство представляет собой массив элементов типаWorkflowNormalizationSetting, конструктор которых принимает следующий набор параметров:- NormalizationSource - источник нормализации. Тип
Guid. - Section - имя секции, где хранится нормализуемое значение. Тип
string. - Field - префикс комплексного поля, где хранится значение ID и записывается нормализуемое значение. Тип
string. - NormalizationField - имя поля, куда записывается нормализуемое значение. Тип
string. - IsTable - (опциональный параметр) флаг, определяет, что обрабатывается табличная секция. По умолчанию имеет значение false.
- NormalizationSource - источник нормализации. Тип
Пример¶
public readonly static WorkflowActionDescriptor WfeLoanSetStateDescriptor =
new WorkflowActionDescriptor(WfeLoanSetStateActionTypeID)
{
Group = "Кредитная заявка",
Icon = "Thin411",
Methods = new []
{
CustomMethodDescriptor,
},
NotPersistentModeAllowed = true,
NormalizationSettings = new WorkflowNormalizationSettings(
[
new WorkflowNormalizationSetting(
PlatformNormalizationSources.Roles,
"WfeLoanSetStateRoles",
"Role",
"Name",
true)
])
};
Important
Созданные дескрипторы действия должны быть зарегистрированы в реестре дескрипторов действий WorkflowActionDescriptorRegistry для сервера, клиента и клиента консольных приложений.
Пример регистрации дескриптора действия¶
[Registrator(Tag = RegistratorTag.GroupForServer | RegistratorTag.GroupForClient)]
public sealed class WfeLoanSetStateDescriptorRegistrator :
RegistratorBase
{
#region Base Overrides
/// <inheritdoc/>
public override void FinalizeRegistration()
{
var descriptorRegistry = this.UnityContainer.TryResolve<WorkflowActionDescriptorRegistry>();
descriptorRegistry?.TryRegisterDescriptor(WfeLoanSetStateDescriptor);
}
#endregion
}
Дескриптор метода¶
Если в действиях предполагается использование кастомных методов, описанных в настройках действия, то для дескриптора действия нужно создать дескриптор метода. Данный дескриптор также используется для формирования связи между выгруженным в .csproj проектом и местом хранения самих скриптов в настройках действия.
WorkflowActionMethodDescriptor содержит следующие свойства, которые можно задать:
-
MethodName - имя компилируемого метода.
-
ReturnType - тип возвращаемого результата. По умолчанию имеет значение void.
-
Parameters - набор пар значение типа
string, обозначающих пары тип параметра-имя параметра. Если не задан, метод не имеет дополнительных параметров. -
StorePath - путь к месту в параметрах действия, где хранится текст скрипта. Если задан ComplexDescriptor, то данное поле определяет путь к месту не в параметрах действия, а в строке.
-
ErrorDescription - описание места возникновения ошибки при возникновении ошибки компиляции в данном методе.
-
MethodDescription - описание метода при выгрузке процесса в проект .csproj. Если не задано, описание формируется автоматически.
-
ComplexDescriptor - описание генератора методов, если метод создается по табличной секции.
WorkflowActionMethodDescriptor имеет следующие методы:
WorkflowActionMethodsComplexDescriptor содержит следующие свойства, которые можно задать:
-
ListPath - путь к месту в параметрах действия, где хранится таблица со скриптами. Путь к скрипту внутри строки определяется по
StorePathвнутри описания метода. -
GetMethodNameSuffix - функция, которая принимает строку в виде
IDictionary<string, object>и возвращает суффикс, добавляемый к имени метода. Должен быть задан. -
GetErrorDescription - функция, которая принимает оригинальный
ErrorDescriptionдескриптора метода, строку в видеIDictionary<string, object>и возвращает описание места возникновения ошибки при возникновении ошибки компиляции в данном методе переданной строки. Если не задан, то возвращаетсяErrorDescriptionиз дескриптора метода. -
GetMethodDescription - функция, которая принимает оригинальный
MethodDescriptionдескриптора метода, строку в видеIDictionary<string, object>и номер строки в таблице и возвращает описание сгенерированного метода при выгрузке процесса в проект .csproj. Если не задано, описание формируется автоматически.
Пример генерации метода¶
public static WorkflowActionMethodDescriptor TaskInitMethod = new WorkflowActionMethodDescriptor()
{
ErrorDescription = "Возникла ошибка в скрипте инициализации", // Описание ошибки
MethodName = "InitMethod", // Имя метода
Parameters = new [] { new Tuple<string, string>("CardTask", "task") }, // метод принимает параметр с типом CardTask и именем task
StorePath = new[] { "InitScript" }, // Текст метода будет хранится в поле InitScript
};
Пример генерации методов по таблице¶
public static WorkflowActionMethodDescriptor TaskEventMethod = new WorkflowActionMethodDescriptor()
{
ErrorDescription = "Возникла ошибка в событии {0}", // Описание ошибки
MethodName = "Event_", // Имя метода без суффикса
Parameters = null, // метод без параметров
StorePath = new[] { "Script" }, // Текст метода будет хранится в поле Script в каждой строке
ComplexDescriptor = new WorkflowActionMethodsComplexDescriptor()
{
// Таблица со строками хранится по ключу Events в параметрах действия
ListPath = new[] { "Events" },
// Возвращаем Event_ + идентификатор строки для гарантирования уникальности имени метода
GetMethodNameSuffix = (hash) => hash.TryGet<Guid>("RowID").ToString("N"),
// Возвращаем описание ошибки с указанием имени события, где возникла ошибка
GetErrorDescription =
(text, hash) => LocalizationManager.Format(
text,
WorkflowEngineHelper.Get<string>(hash, "Event", "Name")),
// Формируем описание метода с помощью имени события и номера строки
GetMethodDescription =
(text, index, hash) =>
LocalizationManager.Format(
"Событие {0} на строке {1}",
WorkflowEngineHelper.Get<string>(hash, "Event", "Name"),
index + 1),
},
};
Обработчик действия¶
Далее необходимо реализовать обработчик действия. Для этого необходимо создать свой тип для обработки действия. Он должен наследоваться от WorkflowActionBase. В базовый конструктор необходимо передать объект-дескриптор для данного действия. Создавать два класса для обработки по одному дескриптору нельзя. Реализованный тип необходимо зарегистрировать в Unity с указанием какого-нибудь уникального имени.
Для данного класса можно реализовать или переопределить следующий ряд методов:
-
Execute - основной метод, который выполняет обработку действия. Обязателен для реализации действия.
Метод имеет следующие параметры:-
context (тип
IWorkflowEngineContext) - контекст обработки процесса. -
scriptObject (тип
IWorkflowEngineCompiled) - объект со всеми скомпилированными скриптами действия. Может быть равенnull, если конкретное действие не содержит ни одного скрипта.
-
Пример¶
Пример реализации метода, который берет значения из своих настроек и устанавливает их в карточку:
protected override void Execute(IWorkflowEngineContext context, IWorkflowEngineCompiled scriptObject)
{
if (context.Signal.Type == WorkflowSignalTypes.Default)
{
var card = context.MainCard;
int? stepID = context.Get<int?>("ActionSection", "Step", "ID"),
stageID = context.Get<int?>("ActionSection", "Stage", "ID"),
string stageName = context.Get<string>("ActionSection", "Stage", "Name"),
stepName = context.Get<string>("ActionSection", "Step", "Name");
card.Sections["MainLoanSection"].Fields["ProcessStepID"] = stepID;
card.Sections["MainLoanSection"].Fields["ProcessStepName"] = stepName;
card.Sections["MainLoanSection"].Fields["ProcessStagesID"] = stageID;
card.Sections["MainLoanSection"].Fields["ProcessStagesName"] = stageName;
}
}
-
Compile - метод, используемый системой для формирования дополнительных методов для компилируемых объектов по настройкам действия. Все основные методы действия, завязанные на данных действия должны быть описаны через дескрипторы методов в дескрипторе действия. Метод возвращает
true, если есть методы для компиляции, иначе возвращаетfalse.
Метод имеет следующие параметры:-
builder (тип
IWorkflowCompilationSyntaxTreeBuilder) - объект, используемый для добавления новых методов для компиляции действия. -
action (тип
WorkflowActionStorage) - действие со всеми его параметрами.
-
Пример¶
Пример реализации метода:
public override bool Compile(IWorkflowCompilationSyntaxTreeBuilder builder, WorkflowActionStorage action)
{
// Вызываем базовую реализацию метода
var result = base.Compile(builder, action);
// Получаем код скрипта
var methodBody = action.Hash.TryGet<string>("Script");
// Добавляем дополнительный метод только если задан основной метод скрипта
if (!string.IsNullOrWhiteSpace(methodBody))
{
// Добавляем новый метод, возвращающий void с именем Script и без параметров.
// В случае ошибки компиляции выведется сообщения вида: Ошибка в сценарии {0}, где {0} - имя объекта, в котором произошла ошибка, например "Ошибка в сценарии в действии "WorkflowScriptAction""
builder
.AddMethod(
"void",
"ScriptWithCallInfo",
null,
body: $"AddInfo(\"Method Script called\"); {methodBody}",
errorDescription: "Ошибка в сценарии {0}");
return true;
}
return result;
}
-
PrepareForExecute - метод, используемый системой при создании нового экземпляра действия. Используется для того, чтобы очистить новый экземпляр действия от ненужных параметров.
Метод имеет следующие параметры:-
actionState (тип
WorkflowActionStateStorage) - объект экземпляра процесса. -
context (тип
IWorkflowEngineContext) - контекст обработки процесса.
-
Пример¶
Пример реализации метода:
public override void PrepareForExecute(WorkflowActionStateStorage actionState, IWorkflowEngineContext context)
{
// Удаляем значение по ключу Script, т.к. сам текст скрипта нам не нужен для каждого экземпляра.
actionState.Hash.Remove("Script");
}
-
PrepareForSave - метод, используемый системой при сохранении версии шаблона. Позволяет как-то модифицировать шаблон процесса при сохранении действия.
Метод имеет следующие параметры:-
action (тип
WorkflowActionStorage) - сохраняемое действие. -
node (тип
WorkflowNodeStorage) - сохраняемый узел, к которому относится данное действие. -
process (тип
WorkflowProcessStorage) - сохраняемый шаблон процесса, к которому относится данный узел.
-
-
CheckActive - метод, используемый системой для проверки, является ли данное действие активным. Если хотя бы одно действие узла активно, экземпляр узла не удаляется.
Метод имеет следующие параметры:- context (тип
IWorkflowEngineContext) - контекст обработки процесса.
- context (тип
Пример¶
Пример реализации метода:
protected override bool CheckActive(IWorkflowEngineContext context)
{
// Если в параметрах действия задан параметр с именем TaskID, то действие считается активным
// За наличие или отсутствие данного значения обычно отвечает само действие (в методе Execute)
return context.Get<Guid?>("TaskID").HasValue;
}
Обработчик редактора действия¶
Также необходимо реализовать обработчик для редактора действия. Для этого необходимо создать свой тип для обработки редактора действия. Он должен наследоваться от WorkflowActionUIHandlerBase. В базовый конструктор необходимо передать объект-дескриптор для данного действия. Создавать два класса для обработки по одному дескриптору нельзя. Реализованный тип необходимо зарегистрировать в Unity с указанием какого-нибудь уникального имени.
Для данного класса можно реализовать или переопределить следующий ряд методов:
-
UpdateFormAsync - метод обновления формы редактора действия при ее первом открытии. Может использоваться для организации работы UI редактора действия. Реализация по умолчанию ничего не делает.
Метод имеет следующие параметры:-
action (тип
WorkflowStorageBase) - редактируемое действие. Может иметь типWorkflowActionStorageилиWorkflowActionStateStorageв зависимости от того, происходит редактирование шаблона или экземпляра действия. -
node (тип
WorkflowStorageBase) - узел, которому принадлежит редактируемое действие. Может иметь типWorkflowNodeStorageилиWorkflowNodeStateStorageв зависимости от того, происходит редактирование шаблона или экземпляра узла. -
process (тип
WorkflowStorageBase) - редактируемый процесс. Может иметь типWorkflowProcessStorageилиWorkflowProcessStateStorageв зависимости от того, происходит редактирование шаблона или экземпляра процесса. -
cardModel (тип
ICardModel) - модель карточки редактора действия. Содержит в себе все контролы редактора и объект карточки, сгенерированный для действия. -
actionTemplate (тип
WorkflowActionStorage) - шаблон действия, к которому относится редактируемый экземпляр. Данный параметр передается только, если происходит редактирование экземпляра действия. -
cancellationToken (тип
CancellationToken) - объект, с помощью которого можно отменить асинхронную задачу.
-
Пример¶
Пример реализации метода, в котором мы очищаем значение одного поля, если производится установка значения другого поля:
public override Task UpdateFormAsync(
WorkflowStorageBase action,
WorkflowStorageBase node,
WorkflowStorageBase process,
ICardModel cardModel,
WorkflowActionStorage actionTemplate = null,
CancellationToken cancellationToken = default)
{
var card = cardModel.Card;
var mainSection = card.Sections["MainSection"];
mainSection.FieldChanged += (s, e) =>
{
switch (e.FieldName)
{
case "Period":
if (e.FieldValue != null)
{
mainSection.Fields["Planned"] = null;
}
break;
case "Planned":
if (e.FieldValue != null)
{
mainSection.Fields["Period"] = null;
}
break;
}
};
Task.CompletedTask;
}
-
InvalidateFormAsync - метод, вызываемый системой при завершении жизни редактора действия. Может использоваться для аннулирования подписок на события или высвобождения неких ресурсов, если действие использует что-то подобное. Реализация по умолчанию ничего не делает.
Метод имеет следующие параметры:-
cardModel (тип
ICardModel) - модель карточки редактора действия. Содержит в себе все контролы редактора и объект карточки, сгенерированный для действия. Имеет значениеnull, если форма редактора действия не открывалась. -
cancellationToken (тип
CancellationToken) - объект, с помощью которого можно отменить асинхронную задачу.
-
-
Validate - метод, вызываемый при валидации действия. Возвращает объект
ValidationResultс сообщением о результате валидации. Реализация по умолчанию возвращаетValidationResult.Empty
Метод имеет следующие параметры:-
action (тип
WorkflowActionStorage) - действие, валидация которого происходит. -
node (тип
WorkflowNodeStorage) - узел, которому принадлежит проверяемое действие. -
process (тип
WorkflowStorageBase) - процесс, к которому принадлежит проверяемое действие.
-
Пример¶
Пример реализации метода, в котором мы проверяем заполнение поля Сценарий. В методе используется вспомогательный метод GetValidateFieldMessage, который возвращает сформированное сообщение о том, что поле не заполнено в конкретном действии конкретного узла.
public override ValidationResult Validate(
WorkflowActionStorage action,
WorkflowNodeStorage node,
WorkflowProcessStorage process)
{
if (string.IsNullOrWhiteSpace(action.Hash.TryGet<string>("Script")))
{
return ValidationResult.FromText(
GetValidateFieldMessage(action, node, "Сценарий"),
ValidationResultType.Warning);
}
return ValidationResult.Empty;
}
-
AttachToCardCore - метод, вызываемый системой для организации привязки данных карточки редактора действия и параметров действия, а также для создания привязки параметров действия к связям. Реализация действия по умолчанию организует привязку параметров по следующим правилам:
-
Все строковые секции содержатся в структуре вида
SectionName: { FieldName1: Value1, -- Физическая колонка FieldName2: Value2, -- Физическая колонка ... ComplexFieldName: { -- Комплексная колонка FieldName1: Value1 -- Физическая колонка комплексной колонки (имя без префикса) ... } }
-
Все строки и поля табличных секций содержатся в структуре вида
SectionName: [ { -- Строка 1 RowID: <RowID>, -- ID строки FieldName1: Value1, -- Физическая колонка FieldName2: Value2, -- Физическая колонка ... ComplexFieldName: { -- Комплексная колонка FieldName1: Value1 -- Физическая колонка комплексной колонки (имя без префикса) ... } }, { -- Строка 2 RowID: ... ... }, ... ]
-
Метод имеет следующие параметры:
- bindingContext (тип
WorkflowEngineBindingContext) - контекст привязки параметров к карточке редактора.
Для организации своей привязки параметров можно использовать следующие методы:
-
AttachEntrySection - производит привязку строковой секции с указанным именем по указанному пути.
-
AttachTableSection - производит привязку табличной секции с указанным именем по указанному пути.
-
AttachField - производит привязку поля строковой секции с заданным именем и типом по указанному пути. Предварительно нужно заполнить
bindingContext.Sectionдля использования данного метода. -
AttachTableSectionToTemplate - производит привязку табличной секции по указанному пути к шаблону действия. Это означает, что данную табличную секцию можно редактировать только при изменении шаблона и редактирование табличной секции для экземпляра недопустимо. Обычно используется для секций, данные которых не должны передаваться в экземпляр действия.
-
AttachFieldToTemplate - привязку поля строковой секции с заданным именем и типом по указанному пути к шаблону действия. Это означает, что данное поле можно редактировать только при изменении шаблона и его редактирование для экземпляра недопустимо. Обычно используется для полей, данные которых не должны передаваться в экземпляр действия.
Для организации привязки параметров к связям следует использовать следующий метод:
- AddLinkBinding - производит привязку параметра по заданному в
fieldPathпути (если параметр принадлежит коллекционной секции, следует использовать версию с дополнительным параметромlistPath, который обозначает путь к месту хранения коллекционной секции) к связям процесса.
Привязка параметров к связям процесса позволяет своевременно обновлять параметры, которые имеют ссылку на связь.
Если необходимо переопределить способ привязки для одного поля/секции, а для остальных использовать поведение по умолчанию, то в своей версии метода AttachToCardCore вызов базовой версии метода base.AttachToCardCore нужно производить в самом конце.
Пример¶
Пример реализации метода для действия задания:
protected override void AttachToCardCore(WorkflowEngineBindingContext bindingContext)
{
// Привязываем поле InitTaskScript к шаблону, предварительно указав секцию, в которой производится привязка
bindingContext.Section = bindingContext.Card.Sections.GetOrAdd(WorkflowActionTypes.TaskSectionName);
AttachFieldToTemplate(
bindingContext,
"InitTaskScript",
typeof(string),
WorkflowActionTypes.TaskSectionName,
"InitTaskScript");
// Привязываем оставшиеся поля секции
AttachEntrySection(
bindingContext,
WorkflowActionTypes.TaskSectionName,
WorkflowActionTypes.TaskSectionName);
// Привязываем табличные секции к шаблону действия
AttachTableSectionToTemplate(
bindingContext,
WorkflowActionTypes.TaskOptionsSectionName,
WorkflowActionTypes.TaskOptionsSectionName);
AttachTableSectionToTemplate(
bindingContext,
WorkflowActionTypes.TaskOptionLinksSectionName,
WorkflowActionTypes.TaskOptionLinksSectionName);
AttachTableSectionToTemplate(
bindingContext,
WorkflowTaskActionBase.EventsSectionName,
WorkflowTaskActionBase.EventsSectionName);
// Указываем привязку к связям поля LinkID из секции с вариантами завершения
AddLinkBinding(bindingContext, new [] { "Link", "ID" }, WorkflowActionTypes.TaskOptionLinksSectionName);
}
Создание расширений на проверку доступа к тайлам¶
Данный вид расширений используется для реализации дополнительной проверки доступа к кнопкам процесса с возможностью настройки доступа через редактор кнопки. Если в шаблоне процесса расширение не подключено, то для тайлов данного шаблона процесса проверки данного расширения выполняться не будут.
Для создания своего расширения на проверку доступа к тайлам нужно реализовать новый класс, реализующий интерфейс IWorkflowEngineTileManagerExtension и зарегистрировать его в UnityContainer и тип карточки, содержащий структуру данных и интерфейс для редактирования, который подставляется в форму настройки кнопки. Для того, чтобы данные расширения отображались в колонке Настройки таблицы кнопок бизнес-процесса, нужно также создать свой класс, реализующий интерфейс IWorkflowEngineTileManagerUIExtension и зарегистрировать его в UnityContainer.
Тип карточки с расширением должен опираться на не виртуальные секции для хранения данных, а также должен иметь секцию BusinessProcessButtonExtension со всеми ее полями.
Интерфейс IWorkflowEngineTileManagerExtension имеет следующие свойства:
-
ID (тип данных Guid) - уникальный идентификатор расширения.
-
ExtensionTypeID (тип данных Guid) - идентификатор типа карточки расширения.
-
Name (тип данных string) - отображаемое имя расширения.
-
Order (тип данных int) - определяет порядок отображения элементов управления в окне настройки кнопки.
Интерфейс IWorkflowEngineTileManagerExtension имеет следующие методы:
-
CheckTileAccessForVisibility - метод для проверки доступа к группе тайлов на видимость. Данный метод получает список идентификаторов проверяемых тайлов и карточку, для который выполняется проверка. Для глобальных тайлов объект
cardравенnull. Метод возвращает список идентификаторов тайлов, для которых проверка прошла успешно.
Пример реализации метода для проверки состояния карточки:public override IEnumerable<Guid> CheckTileAccessForVisibility(List<Guid> tileIDs, Card card) { // Если проверка идет вне контекста карточки, то возвращаем полный список тайлов if (card == null) { return tileIDs; }
using (dbScope.Create()) { var db = dbScope.Db;
if (card.Sections.TryGetValue("KrApprovalCommonInfoVirtual", out var krSection) && krSection.Fields.TryGetValue("StateID", out var stateIDObj)) { var stateID = stateIDObj == null ? 0 : (int)stateIDObj;
// запрос на проверку состояния карточки return db.SetCommand( dbScope.BuilderFactory .Select().C("bpbe", "ButtonRowID") .From("KrCheckStateTileExtension", "s").NoLock() .InnerJoin("BusinessProcessButtonExtension", "bpbe").NoLock() .On().C("s", "ID").Equals().C("bpbe", "ID") .Where().C("s", "StateID").Equals().P("StateID").And().C("bpbe", "ButtonRowID").In(tileIDs) .Build(), db.Parameter("StateID", stateID)) .LogCommand() .ExecuteScalarList<Guid>(); } }
// Если карточка не имеет состояния, то возвращаем пустой список return EmptyHolder<Guid>.Collection; }
-
CheckTileAccessForExecute - метод для проверки доступа на исполнение конкретного тайла. Метод возвращает результат валидации проверки доступа.
Данный метод получает в качестве параметров:-
tileInfo(тип данных WorkflowEngineTile) - объект со всеми данными исполняемого тайла. -
cardID(тип данных Guid) - идентификатор карточки, в рамках которой происходит исполнение тайла. -
cardGetter(тип данных Func<Card>) - функция для получения объекта карточки.
Пример реализации метода для проверки состояния карточки:public override ValidationResult CheckTileAccessForExecute(WorkflowEngineTile tileInfo, Guid cardID, Func<Card> cardGetter) { using(dbScope.Create()) { var result = db.SetCommand( dbScope.BuilderFactory .Select().Top(1).V(true) .From("KrCheckStateTileExtension", "s").NoLock() .InnerJoin("BusinessProcessButtonExtension", "bpbe").NoLock() .On().C("bpbe", "ID").Equals().C("s", "ID") .LeftJoin("KrApprovalCommonInfo", "kr").NoLock() .On().C("kr", "MainCardID").Equals().P("CardID") .Where().C("s", "StateID").Equals().Coalesce(b => b.C("kr", "StateID").V(0)) .And().C("bpbe", "ButtonRowID").Rquals().P("TileID") .Build(), db.Parameter("CardID", cardID), db.Parameter("TileID", tileInfo.TileID)) .LogCommand() .ExecuteScalar<bool?>() ?? false;
if (result) { return ValidationResult.Empty; } else { return ValidationResult.FromText( this, "$KrTileExtensions_CheckState_AccessDenied", ValidationResultType.Error); } } }
-
Интерфейс IWorkflowEngineTileManagerUIExtension имеет следующие свойства:
- ID (тип данных Guid) - уникальный идентификатор расширения. Должен соответствовать ID расширения, указанном в классе расширения, реализующем
IWorkflowEngineTileManagerExtension.
Интерфейс IWorkflowEngineTileManagerUIExtension имеет следующие методы:
-
ModifyButtonRow - Метод для модификации значения в колонке Настройки для строки из таблицы кнопок бизнес-процесса.
Данный метод получает в качестве параметров:-
context(тип данных IWorkflowEngineTileManagerUIExtensionContext) - контекст для обработки расширенийIWorkflowEngineTileManagerUIExtension. Для добавления данных следует использоватьStringBuilderпо свойствуcontext.Result.
Пример реализации метода:public void ModifyButtonRow(IWorkflowEngineTileManagerUIExtensionContext context) { // Если для интересующей нас секции нет строк с данными, то в настройки ничего не записываем if (context.AllButtonRows.TryGetValue(extendedSectionName, out var contextRoleRows) && contextRoleRows.Count > 0) { // Если другие настройки уже произвели запись результата, то записываем с новой строки if (context.Result.Length > 0) { context.Result.AppendLine(); }
context.Result.Append(LocalizationManager.Localize("$KrTileExtensions_CheckState_States")); context.Result.Append(string.Join("; ", contextRoleRows.Select(x => LocalizationManager.Localize((string)x["StateName"])))); } }
-
Создание обработчика тайла¶
В системе есть возможность создавать обработчики тайлов. Они позволяют дополнить или переопределить клиентскую логику обработки тайла, а также дополнительно ограничить условие видимости тайла.
Для создания своего обработчика тайла нужно:
- Создать дескриптор обработчика тайла, содержащий всю необходимую информацию о данном обработчике.
- Создать и зарегистрировать серверный обработчик тайла.
- Создать и зарегистрировать клиентский обработчик тайла.
Создание дескриптора обработчика тайла¶
Для создания дескриптора обработчика тайла нужно создать новый объект с типом WorkflowEngineTileHandlerDescriptor. Данный объект содержит свойства обработчика тайла, а также используется для регистрации обработчика на сервере и клиенте.
Дескриптор содержит следующие свойства:
- ID - уникальный идентификатор обработчика;
- Name - имя обработчика. Используется для отображения имени выбранного обработчика в настройках кнопки бизнес-процесса. Может быть строкой локализации.
Example
Пример создания дескриптора обработчика тайла.
using Tessa.Workflow.Tiles;
namespace Tessa.Extensions.Shared
{
/// <summary>
/// Дескрипторы обработчиков тайлов WorkflowEngine.
/// </summary>
public static class WorkflowEngineTileHandlerDescriptors
{
#region Descriptors
/// <summary>
/// Дескриптор обработчика-примера.
/// </summary>
public static readonly WorkflowEngineTileHandlerDescriptor ExampleHandler = new()
{
ID = new System.Guid(// {89ECF23A-2A31-4A95-B2A7-1150E748981C}
0x89ecf23a, 0x2a31, 0x4a95, 0xb2, 0xa7, 0x11, 0x50, 0xe7, 0x48, 0x98, 0x1c),
Name = "Example handler",
};
#endregion
}
}
Создание серверного обработчика тайла¶
Для создания серверного обработчика тайла нужно реализовать новый класс, реализующий интерфейс IWorkflowEngineTileHandler, и зарегистрировать его в IWorkflowEngineTileHandlerResolver с указанием дескриптора обработчика тайла.
Серверный обработчик имеет следующие методы:
-
CheckVisibilityAsync - данный метод выполняет проверку видимости тайла, для которого указан текущий обработчик. Метод возвращает значение
true, если тайл должен быть виден, иначе он возвращаетfalse. Логика проверки видимости тайла не вызывается, если тайл недоступен по другим причинам.Данный метод имеет следующие параметры:
- tile - объект со всей информацией о тайле;
- card - карточка, для которой выполняется проверка видимости тайла. Может быть не указана, если выполняется проверка видимости глобального тайла.
Example
Пример создания серверного обработчика тайла.
using System;
using System.Threading;
using System.Threading.Tasks;
using Tessa.Cards;
using Tessa.Platform.Storage;
using Tessa.Workflow;
using Tessa.Workflow.Tiles;
namespace Tessa.Extensions.Server
{
public class WorkflowEngineTileHandlerExample : IWorkflowEngineTileHandler
{
#region IWorkflowEngineTileHandler Implementation
/// <inheritdoc/>
public ValueTask<bool> CheckVisibilityAsync(
WorkflowEngineTile tile,
Card? card,
CancellationToken cancellationToken = default)
{
// Если у карточки есть секция DocumentCommonInfo, то отображаем тайл только в случае, когда не задан контрагент
var result = card?.Sections.TryGetValue("DocumentCommonInfo", out var dciSection) == true
? dciSection.RawFields.TryGet<Guid?>("PartnerID") is null
: true;
return ValueTask.FromResult(result);
}
#endregion
}
}
Example
Пример регистрации серверного обработчика тайла.
using Tessa.Extensions.Shared;
using Tessa.Platform;
using Tessa.Workflow.Tiles;
using Unity;
namespace Tessa.Extensions.Server
{
[Registrator]
public sealed class Registrator : RegistratorBase
{
#region Base Overrides
public override void RegisterUnity()
{
this.UnityContainer
.RegisterSingleton<WorkflowEngineTileHandlerExample>()
;
}
public override void FinalizeRegistration()
{
this.UnityContainer
.TryResolve<IWorkflowEngineTileHandlerResolver>()?
.Register<WorkflowEngineTileHandlerExample>(WorkflowEngineTileHandlerDescriptors.ExampleHandler)
;
}
#endregion
}
}
Создание клиентского обработчика тайла для desktop-клиента¶
Для создания клиентского обработчика тайла для desktop-клиента нужно реализовать новый класс, реализующий интерфейс IWorkflowEngineTileUIHandler, и зарегистрировать его в IWorkflowEngineTileUIHandlerResolver с указанием дескриптора обработчика тайла.
Клиентский обработчик имеет следующие методы:
-
HandleTileAsync - данный метод обрабатывает нажатие на тайл бизнес-процесса, для которого указан текущий обработчик. Метод возвращает значение
true, если в дальнейшем должна быть вызвана стандартная логика обработки тайла бизнес-процесса, иначе он возвращаетfalse.Данный метод имеет следующие параметры:
- context - текущий контекст операции с пользовательским интерфейсом. При обработке нажатия кнопки бизнес-процесса из карточки содержит редактор карточки, для которой выполняется нажатие кнопки;
- tileInfo - объект со всей информацией о тайле;
- additionalInfo - дополнительная информация, отправляемая при стандартной обработке тайла.
Example
Пример создания клиентского обработчика тайла для desktop-клиента.
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tessa.UI;
using Tessa.UI.Workflow;
using Tessa.Workflow.Storage;
namespace Tessa.Extensions.Client
{
public sealed class WorkflowEngineTileUIHandlerExample(IUIHost uiHost) : IWorkflowEngineTileUIHandler
{
#region Fields
private readonly IUIHost uiHost = NotNullOrThrow(uiHost);
#endregion
#region IWorkflowEngineTileUIHandler Implementation
/// <inheritdoc/>
public async ValueTask<bool> HandleTileAsync(
IUIContext context,
WorkflowTileInfo tileInfo,
Dictionary<string, object?> additionalInfo,
CancellationToken cancellationToken = default)
{
if (context.CardEditor?.CardModel?.Card is not { } card
|| !card.Sections.TryGetValue("DocumentCommonInfo", out var dciSection))
{
return true;
}
var selectedValue = await this.uiHost.ShowViewsDialogAsync(
"Partners",
cancellationToken: cancellationToken);
if (selectedValue is null)
{
return false;
}
var partnerID = selectedValue.Value;
var partnerName = selectedValue.DisplayText;
dciSection.Fields["PartnerID"] = partnerID;
dciSection.Fields["PartnerName"] = partnerName;
return true;
}
#endregion
}
}
Example
Пример регистрации клиентского обработчика тайла для desktop-клиента.
using Tessa.Extensions.Shared;
using Tessa.Platform;
using Tessa.UI.Workflow;
using Unity;
namespace Tessa.Extensions.Client
{
[Registrator]
public sealed class RegistratorTest : RegistratorBase
{
#region Base Overrides
public override void RegisterUnity()
{
this.UnityContainer
.RegisterSingleton<WorkflowEngineTileUIHandlerExample>()
;
}
public override void FinalizeRegistration()
{
this.UnityContainer
.TryResolve<IWorkflowEngineTileUIHandlerResolver>()?
.Register<WorkflowEngineTileUIHandlerExample>(WorkflowEngineTileHandlerDescriptors.ExampleHandler)
;
}
#endregion
}
}
Создание клиентского обработчика тайла для web-клиента¶
Для создания клиентского обработчика тайла для web-клиента нужно реализовать новый класс, реализующий интерфейс IWorkflowEngineTileUIHandler, и зарегистрировать его в WorkflowEngineTileUIHandlerRegistry с указанием идентификатора обработчика. В отличии от сервера и desktop-клиента, регистрация обработчика на web-сервере производится по идентификатору обработчика, указанному в дескрипторе обработчика на сервере.
Клиентский обработчик имеет следующие методы:
-
handleTile - данный метод обрабатывает нажатие на тайл бизнес-процесса, для которого указан текущий обработчик. Метод возвращает значение
true, если в дальнейшем должна быть вызвана стандартная логика обработки тайла бизнес-процесса, иначе он возвращаетfalse.Данный метод имеет следующие параметры:
- context - текущий контекст операции с пользовательским интерфейсом. При обработке нажатия кнопки бизнес-процесса из карточки содержит редактор карточки, для которой выполняется нажатие кнопки;
- tileInfo - объект со всей информацией о тайле;
- additionalInfo - дополнительная информация, отправляемая при стандартной обработке тайла.
Example
Пример создания клиентского обработчика тайла для web-клиента.
import { FieldType, IStorage } from '@tessa/core';
import { WorkflowTileInfo } from '@tessa/platform';
import { IUIContext } from 'tessa/ui';
import { showViewsDialog } from 'tessa/ui/uiHost';
import { IWorkflowEngineTileUIHandler } from 'tessa/ui/workflow';
import { SelectedValue } from 'tessa/views';
export class WorkflowEngineTileUIHandlerExample implements IWorkflowEngineTileUIHandler {
//#region IWorkflowEngineTileUIHandler Implementation
async handleTile(
context: IUIContext,
_tileInfo: WorkflowTileInfo,
_additionalInfo: IStorage
): Promise<boolean> {
const card = context.cardEditor?.cardModel?.card;
if (!card) {
return true;
}
const dciSection = card.sections.tryGet('DocumentCommonInfo');
if (!dciSection) {
return true;
}
let selectedValue: SelectedValue | null = null;
await showViewsDialog('Partners', (value: SelectedValue) => {
selectedValue = value;
return Promise.resolve();
});
if (!selectedValue) {
return false;
}
const partnerId = (selectedValue as SelectedValue).value;
const partnerName = (selectedValue as SelectedValue).displayText;
dciSection.fields.set('PartnerID', partnerId, FieldType.Guid);
dciSection.fields.set('PartnerName', partnerName, FieldType.String);
return true;
}
//#endregion
}
Example
Пример регистрации клиентского обработчика тайла для web-клиента.
import { ExtensionRegistrator } from '@tessa/application';
import { WorkflowEngineTileUIHandlerRegistry } from 'tessa/ui/workflow';
import { WorkflowEngineTileUIHandlerExample } from './workflowEngineTileUIHandlerExample';
export const ExamplesRegistrator: ExtensionRegistrator = {
async registerTypes(_container) {
WorkflowEngineTileUIHandlerRegistry.instance.register(
'89ecf23a-2a31-4a95-b2a7-1150e748981c',
new WorkflowEngineTileUIHandlerExample()
);
}
};
Создание кастомного базового класса процесса¶
Для создания своего базового класса процесса нужно реализовать новый класс, наследуемый от WorkflowEngineCompiledBase, и зарегистрировать его в IWorkflowEngineCompiledBaseRegistry. Реализованный базовый класс обязательно должен иметь атрибут public и не иметь атрибута sealed, а также иметь конструктор без параметров.
При регистрации базового класса необходимо обязательно задать уникальный ID, который будет обозначать данный базовый класс, а также отображаемое имя класса, которое увидит пользователь.
Пример¶
Ниже пример базового класса с дополнительным методом для установки некоторых параметров переданного задания в сигнал:
using Tessa.Cards;
using Tessa.Workflow.Compilation;
namespace Tessa.Extensions.Demo.Server.WorkflowEngine.Wfe
{
public class WfeCompiledBase : WorkflowEngineCompiledBase
{
public void SetParams(CardTask task)
{
Signal.TaskID = task.RowID;
Signal.PerformerID = task.UserID;
}
}
}
Вот пример его регистрации:
this.UnityContainer.Resolve<IWorkflowEngineCompiledBaseRegistry>()
.Register<WfeCompiledBase>(new Guid("8555B8AA-A035-45F8-B216-0F96D443D07E"), "Тестовый базовый класс")
;