Новое событие с номером, вызываемое из бизнес-процесса
Новое событие с номером, вызываемое из бизнес-процесса¶
Предположим, что есть определённый скрипт бизнес-процесса или тип этапа маршрута, в котором надо выполнить действие с номерами в контексте INumberDirector, например, зарезервировать и выделить номер типа NumberTypes.Primary в соответствии с настройками нумерации для типа документа.
Для этого требуется определить новый идентификатор NumberEventType
и связать его с методом в NumberDirector
. Идентификатор определим в сборке Tessa.Extensions.Shared
в статическом классе MyEventTypes
:
using System;
using Tessa.Cards.Numbers;
namespace Tessa.Extensions.Shared.Numbers
{
public static class MyEventTypes
{
public static readonly NumberEventType CustomEvent =
new NumberEventType(new Guid("уникальный ID для этого события"), nameof(CustomEvent));
public static void Register(INumberEventTypeRegistry registry)
{
registry.Register(CustomEvent);
}
}
}
Бизнес-логика действия с номером определяется в классе-наследнике DocumentNumberDirector
, в котором мы добавим новый метод NotifyOnCustomEvent
. Класс также добавим в сборку Tessa.Extensions.Shared
.
using System.Threading;
using System.Threading.Tasks;
using Tessa.Cards;
using Tessa.Cards.Numbers;
using Tessa.Extensions.Default.Shared.Numbers;
using Tessa.Extensions.Default.Shared.Workflow.KrProcess;
using Tessa.Platform;
namespace Tessa.Extensions.Shared.Numbers
{
public class MyNumberDirector : DocumentNumberDirector
{
public MyNumberDirector(IKrTypesCache typesCache, INumberDependencies dependencies)
: base(typesCache, dependencies)
{ }
public Task<bool> NotifyOnCustomEventAsync(
INumberContext context, CancellationToken cancellationToken = default) =>
this.NotifyOnEventAsync(
context, MyEventTypes.CustomEvent,
this.OnCustomEventAsync, this.BeforeCustomEventAsync, cancellationToken);
protected virtual Task<bool> BeforeCustomEventAsync(
INumberContext context, CancellationToken cancellationToken = default) => TaskBoxes.True;
protected virtual async Task<bool> OnCustomEventAsync(
INumberContext context, CancellationToken cancellationToken = default)
{
// здесь размещается бизнес-логика для нашего события, см. по аналогии
// с методами DocumentNumberDirector.OnRegisteringCard и др.
INumberObject reservedNumber = await context.Builder.ReserveAndCommitAtServerAsync(
context, NumberTypes.Primary, cancellationToken: cancellationToken).ConfigureAwait(false);
return !reservedNumber.IsEmpty();
}
protected override async ValueTask<bool> IsAvailableCoreAsync(
INumberContext context,
NumberEventType eventType,
CancellationToken cancellationToken = default)
{
// здесь можно дополнительно указать предикат - проверки перед тем, как будет вызвано событие,
// чтобы заблокировать его вызов в каких-то ситуациях
// в этом примере указывается, что запрещено вызывать событие при первом сохранении карточки;
// другие примеры см. в базовой реализации в DocumentNumberDirector
// если такие проверки не нужны, то не переопределяйте метод IsAvailableCore
if (!await base.IsAvailableCoreAsync(context, eventType, cancellationToken).ConfigureAwait(false))
{
return false;
}
if (eventType == MyEventTypes.CustomEvent
&& context.Card.StoreMode == CardStoreMode.Insert)
{
return false;
}
return true;
}
}
}
Зарегистрируем тип события, а также класс MyNumberDirector
, который будет функционировать для типов карточки Contract
(“Договор”) и Outgoing
(“Исходящий”).
using System;
using Tessa.Cards.Numbers;
using Tessa.Platform;
using Unity;
using Unity.Lifetime;
namespace Tessa.Extensions.Shared.Numbers
{
[Registrator]
public sealed class Registrator : RegistratorBase
{
private static readonly Guid[] ids =
{
// Contract
new Guid("335f86a1-d009-012c-8b45-1f43c2382c2d")
};
public override void InitializeRegistration() => MyEventTypes.Register(NumberEventTypeRegistry.Instance);
public override void RegisterUnity()
{
this.UnityContainer
.RegisterType<MyNumberDirector>(new ContainerControlledLifetimeManager());
}
public override void FinalizeRegistration()
{
var container = this.UnityContainer.TryResolve<INumberDirectorContainer>();
if (container is not null)
{
foreach (Guid cardTypeID in ids)
{
container.Register(cardTypeID, c => c.Resolve<AdditionalNumberDirector>());
}
}
}
}
}
Теперь требуется вызвать метод MyNumberDirector.NotifyOnCustomEvent
из другого места системы, передав ему требуемые зависимости. Рассмотрим ситуацию, когда действие с номером выполняется в новом типе этапа при завершении задания на этом этапе.
Для типа этапа в сборке Tessa.Extensions.Shared
указывается дескриптор типа этапа с настройками, которые система маршрутов использует для этого типа. Также здесь обязательно добавить строку в таблицу KrProcessStageTypes
, см. комментарий.
using System;
using Tessa.Extensions.Default.Shared.Workflow;
using Tessa.Extensions.Default.Shared.Workflow.KrProcess;
namespace Tessa.Extensions.Shared.Numbers
{
public static class MyStageTypeDescriptors
{
// продублируйте значения ID и Caption этого типа этапа в таблице KrProcessStageTypes,
// добавив строку в таблицу для проектной библиотеки схемы
public static readonly StageTypeDescriptor MyStageDescriptor =
StageTypeDescriptor.Create(b =>
{
b.ID = new Guid("уникальный ID для этого типа этапа");
b.Caption = "Мой этап";
b.SettingsCardTypeID = new Guid("ID типа карточки с настройками этапа");
b.PerformerUsageMode = PerformerUsageMode.Multiple;
b.PerformerIsRequired = true;
b.PerformerCaption = "$UI_KrPerformersSettings_Approvers";
b.CanOverrideAuthor = true;
b.UseTimeLimit = true;
b.CanOverrideTaskHistoryGroup = true;
b.SupportedModes.Add(KrProcessRunnerMode.Async);
});
}
}
Note
Для типа этапа в сборке Tessa.Extensions.Shared
также можно указать объект-форматтер, посредством которого заполняется колонка “Настройки” в таблице этапов, в зависимости от указанных настроек. Для desktop-клиента форматтер будет вызван сразу после закрытия редактируемой строки, для web-клиента - на сервере при сохранении карточки с маршрутом. Если форматтер не указан, то поле с настройками будет пустым. Пример форматтера доступен в классе RegistrationStageTypeFormatter
, а его регистрация в классе StageTypeFormattersRegistrator
.
Для типа этапа задаётся обработчик - класс-наследник StageTypeHandlerBase
, который система маршрутов вызывает при действиях с этапом. Добавьте такой класс в Tessa.Extensions.Server
.
using System.Threading.Tasks;
using Tessa.Cards;
using Tessa.Cards.Extensions;
using Tessa.Cards.Numbers;
using Tessa.Extensions.Default.Server.Workflow.KrProcess.Scope;
using Tessa.Extensions.Default.Server.Workflow.KrProcess.Workflow.Handlers;
using Tessa.Platform;
namespace Tessa.Extensions.Server.Numbers
{
public class MyStageTypeHandler : StageTypeHandlerBase
{
public MyStageTypeHandler(INumberDirectorContainer numberContainer, IKrScope krScope, ICardMetadata cardMetadata)
{
this.NumberContainer = numberContainer;
this.KrScope = krScope;
this.CardMetadata = cardMetadata;
}
protected INumberDirectorContainer NumberContainer { get; set; }
protected IKrScope KrScope { get; set; }
protected ICardMetadata CardMetadata { get; set; }
// переопределите здесь другие методы, которые отправляют задания,
// по аналогии с RegistrationStageTypeHandler
public override async Task<StageHandlerResult> HandleTaskCompletionAsync(IStageTypeHandlerContext context)
{
if (context.MainCardID.HasValue && context.MainCardTypeID.HasValue)
{
CardType cardType = (await this.CardMetadata.GetCardTypesAsync(context.CancellationToken))[context.MainCardTypeID.Value];
// если для типа карточки, в котором задействован этап, зарегистрирован MyNumberDirector
// в качестве объекта для взаимодействия с номерами, то подготовим контекст
INumberDirectorProvider numberProvider = this.NumberContainer.GetProvider(cardType.ID);
INumberDirector numberDirector = numberProvider.GetDirector();
Card mainCard = await this.KrScope.GetMainCardAsync(context.MainCardID.Value, cancellationToken: context.CancellationToken);
INumberComposer numberComposer = numberProvider.GetComposer();
INumberContext numberContext = await numberDirector
.CreateContextAsync(
numberComposer,
mainCard,
cardType,
context.CardExtensionContext is ICardStoreExtensionContext storeContext
? storeContext.Request.Info
: null,
transactionMode: NumberTransactionMode.SeparateTransaction,
cancellationToken: context.CancellationToken);
// и вызовем метод NotifyOnCustomEvent, в котором расположена бизнес-логика
// взаимодействия с номерами
await numberDirector.NotifyOnEventAsync(
numberContext,
NumberEventTypes.CustomAction,
(x, ct) => TaskBoxes.True,
cancellationToken: context.CancellationToken);
context.ValidationResult.Add(numberContext.ValidationResult);
}
return StageHandlerResult.CompleteResult;
}
}
}
Тип этапа вместе с его дескриптором регистрируется на сервере Tessa.Extensions.Server
.
using Tessa.Extensions.Default.Server.Workflow.KrProcess.Workflow;
using Tessa.Extensions.Shared.Numbers;
using Tessa.Platform;
using Unity;
using Unity.Lifetime;
namespace Tessa.Extensions.Server.Numbers
{
[Registrator]
public sealed class Registrator : RegistratorBase
{
public override void RegisterUnity()
{
this.UnityContainer
.RegisterType<MyStageTypeHandler>(new ContainerControlledLifetimeManager())
;
}
public override void FinalizeRegistration()
{
this.UnityContainer
.TryResolve<IKrProcessContainer>()
?
.RegisterHandler<MyStageTypeHandler>(MyStageTypeDescriptors.MyStageDescriptor)
;
}
}
}
Таким образом, реализуется любая бизнес-логика с номерами, в т.ч. с добавленными в проекте типами номеров NumberType
, которые могут размещаться в произвольных полях карточки INumberLocation
.
Вызвать метод с такой бизнес-логикой возможно при наличии ссылок на объекты: IUnityContainer
, тип карточки CardType
и экземпляр загруженной карточки Card
. Т.е. вызов возможен практически из любого скрипта или расширения в системе.