Запросы к API нумерации с клиента¶
Рассмотрим сценарий, при котором для созданной, но ещё несохранённой карточки типа Incoming
(“Входящий”) требуется реализовать логику, где по некоторому клиентскому событию (в примере - по изменению ссылки в поле “Подразделение”) должен резервироваться номер из последовательности, связанной с этим подразделением.
При этом, в соответствии с настройками типа документа, при создании карточки уже зарезервирован номер из другой последовательности. Поэтому требуется и дерезервировать предыдущий номер, и добавить записи в клиентскую очередь нумерации (по ключу card.info['.numberQueue']
) о том, что новый номер требуется выделить при сохранении карточки и дерезервировать при закрытии вкладки.
Для реализации такого сценария при наступлении клиентского события требует вызвать cardRequest
к серверному API нумерации, который будет обработан в объекте-реализации INumberDirector
. Действие будет аналогично тому, что выполняет запрос по резервированию номера из контрола “Нумератор”, но дополнительно будет передана информация с клиента (идентификатор выбранного подразделения), которая будет учитываться при резервировании нового номера.
Tip
Аналогичным образом можно реализовывать любые клиентские вызовы к серверному API нумерации, где со стороны клиента можно передавать любую информацию (которую сервер не может получить сам, т.к. эти данные не сохранены), а в серверном INumberDirector
можно использовать как зависимости API нумерации (INumberContext
, INumberComposer
, INumberBuilder
и др.), так и зависимости из DI-контейнера (например, для чтения настроек из карточек настроек и из типа документа).
Реализация со стороны сервера¶
Серверную логику расположим в проекте Tessa.Extensions.Server
. Создадим подпапку Numbers
, где будут располагаться все последующие классы.
Note
В примере не рассматривается использование в desktop-клиенте. При такой необходимости можно часть кода с реализацией INumberDirector
и регистрацией события NumberEventType
перенести в проект Tessa.Extensions.Shared
, а также для асинхронных вызовов добавить .ConfigureAwait(false)
.
Добавим класс AbNumberEventTypes
, в котором укажем тип события ReserveNumberForIncoming
, вызываемого с клиента. Это уникальный идентификатор, который используется и в API нумерации, и как идентификатор типа запроса cardRequest
.
Tip
Здесь Ab
- префикс всех объектов для проектного решения или модуля. Укажите любую короткую аббревиатуру, которая будет добавляться к именам типов, файлов и объектов конфигурации.
using System;
using Tessa.Cards.Numbers;
namespace Tessa.Extensions.Server.Numbers
{
public static class AbNumberEventTypes
{
public static readonly NumberEventType ReserveNumberForIncoming =
new(new Guid(0x3943b50f, 0xd0d8, 0x45ba, 0xb5, 0x95, 0xf4, 0x4f, 0x24, 0x16, 0x6c, 0x81),
nameof(ReserveNumberForIncoming)); // 3943b50f-d0d8-45ba-b595-f44f24166c81
}
}
Добавим класс AbReserveNumberForIncomingRequestExtension
, определяющий обработчик запроса cardRequest
, который отправляется с клиента. Он наследуется от класса NumberControlRequestExtension
, где содержится вся инфраструктурная логика для использования API нумерации.
using System.Threading.Tasks;
using Tessa.Cards.ComponentModel;
using Tessa.Cards.Extensions;
using Tessa.Cards.Numbers;
namespace Tessa.Extensions.Server.Numbers
{
public class AbReserveNumberForIncomingRequestExtension : NumberControlRequestExtension
{
public AbReserveNumberForIncomingRequestExtension(
ICardTransactionStrategy transactionStrategy,
ICardGetStrategy getStrategy,
ICardNewStrategy newStrategy,
INumberDirectorContainer numberDirectorContainer)
: base(transactionStrategy, getStrategy, newStrategy, numberDirectorContainer)
{
}
protected override ValueTask<bool> ExecuteNumberActionAsync(
ICardRequestExtensionContext context,
NumberControlRequest request,
INumberContext numberContext,
INumberDirector numberDirector) =>
numberDirector is AbIncomingNumberDirector incomingNumberDirector
? incomingNumberDirector.NotifyOnReserveNumberForIncomingAsync(
numberContext, context.CancellationToken)
: new(false);
}
}
Здесь в методе ExecuteNumberActionAsync
определяется логика по обработке запроса, которая делегируется объекту INumberDirector
с типом AbIncomingNumberDirector
.
Создадим класс AbIncomingNumberDirector
, содержащий бизнес-логику запроса. Тип наследуется от DocumentNumberDirector
, чтобы реагировать на события нумерации по аналогии с тем, как это производится для документов типового решения.
using System;
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;
using Tessa.Platform.Runtime;
using Tessa.Platform.Storage;
namespace Tessa.Extensions.Server.Numbers
{
public class AbIncomingNumberDirector : DocumentNumberDirector
{
public AbIncomingNumberDirector(IKrTypesCache typesCache, INumberDependencies dependencies)
: base(typesCache, dependencies)
{
}
public ValueTask<bool> NotifyOnReserveNumberForIncomingAsync(
INumberContext context,
CancellationToken cancellationToken = default) =>
this.NotifyOnEventAsync(
context,
AbNumberEventTypes.ReserveNumberForIncoming,
this.OnReserveNumberForIncomingAsync,
this.BeforeReserveNumberForIncomingAsync,
cancellationToken);
protected virtual async ValueTask<bool> OnReserveNumberForIncomingAsync(
INumberContext context,
CancellationToken cancellationToken = default)
{
INumberObject number = context.NumberObject;
bool canReserve = true;
if (number.IsSequential())
{
(bool released, _) = await context.Builder.ReleaseAndCommitAtServerAsync(
context, number, cancellationToken);
if (released)
{
context.SerializableInfo["Released"] = BooleanBoxes.True;
}
canReserve = released;
}
if (!canReserve)
{
return false;
}
await context.Builder.ReserveAndCommitAtServerAsync(
context, number.Type, SessionType.Client, cancellationToken);
return true;
}
protected virtual ValueTask<bool> BeforeReserveNumberForIncomingAsync(
INumberContext context,
CancellationToken cancellationToken = default) => new(true);
protected override async ValueTask<bool> IsAvailableCoreAsync(
INumberContext context,
NumberEventType eventType,
CancellationToken cancellationToken = default)
{
if (eventType == AbNumberEventTypes.ReserveNumberForIncoming)
{
// с клиентской стороны или для уже сохранённой карточки игнорируем запрос;
// ContextInfo аналогичен CardRequest.Info
return context.Session.Type == SessionType.Server
&& context.Card.StoreMode == CardStoreMode.Insert
&& context.ContextInfo.TryGet<Guid?>("DepartmentID") is not null;
}
return await base.IsAvailableCoreAsync(context, eventType, cancellationToken);
}
protected override async ValueTask<string?> TryGetSequenceNameCoreAsync(
INumberContext context,
NumberTypeDescriptor numberType,
CancellationToken cancellationToken = default)
{
if (context.EventType == AbNumberEventTypes.ReserveNumberForIncoming
&& context.ContextInfo.TryGet<Guid?>("DepartmentID") is { } departmentID)
{
return $"Incoming-{departmentID}";
}
return await base.TryGetSequenceNameCoreAsync(context, numberType, cancellationToken);
}
}
}
Рассмотрим определяемые методы:
NotifyOnReserveNumberForIncomingAsync
- “точка входа” для обработки события, этот метод вызывается из запросаAbReserveNumberForIncomingRequestExtension
.OnReserveNumberForIncomingAsync
- алгоритм обработки события. Здесь освобождается предыдущий номер (пришедший с клиента) вызовомReleaseAndCommitAtServerAsync
и резервируется новый номер вызовомReserveAndCommitAtServerAsync
.- В процессе наполняется очередь нумерации
NumberQueue
, которая будет автоматически отправлена на клиент. - Флаг
Released
отправляется на клиент при успешном освобождении предыдущего номера. - Алгоритм этого метода аналогичен стандартному резервированию номера через контрол “Нумератор”, что соответствует серверной части метода
DocumentNumberDirector.OnReservingNumberFromControlAsync
.
- В процессе наполняется очередь нумерации
BeforeReserveNumberForIncomingAsync
- метод, запускаемый перед вызовомOnReserveNumberForIncomingAsync
, в котором возможно выполнить подготовительные действия перед обработкой события. В текущем примере он не используется, но его можно переопределить для наследниковAbIncomingNumberDirector
, если в проекте определяются различные реализацииINumberDirector
для разных типов карточек.IsAvailableCoreAsync
- функция, определяющая доступность события для текущего состояния карточки в соответствии с настройками типа карточки/документа.- Здесь указано, что событие
ReserveNumberForIncoming
доступно только в серверном контексте для ещё несохранённых карточек, и при условии, что в запросеcardRequest
передан идентификатор выбранного подразделения вinfo['DepartmentID']
. - Если функция возвращает
false
, то методыBeforeReserveNumberForIncomingAsync
иOnReserveNumberForIncomingAsync
не будут выполнены, и клиент получит информацию, что запрос неуспешен.
- Здесь указано, что событие
TryGetSequenceNameCoreAsync
- функция, определяющая алиас последовательности номеров, из которой выделяется номер, в т.ч. посредством методаReserveAndCommitAtServerAsync
.- Для события
ReserveNumberForIncoming
номер будет выделяться из последовательности, связанной с подразделением, т.е. для каждого подразделения последовательность будет отдельной. - Для всех прочих событий задействуется логика типового решения из класса
DocumentNumberDirector
.
- Для события
Tip
Чтобы в дополнение к алиасу последовательности задать формат полного номера (отображаемую пользователю строку с номером), переопределите метод GetFullNumberCoreAsync
по аналогии с TryGetSequenceNameCoreAsync
.
Создадим класс Registrator
, в котором выполняется регистрация всех определённых выше типов.
using Tessa.Cards;
using Tessa.Cards.Extensions;
using Tessa.Cards.Numbers;
using Tessa.Extensions.Default.Shared;
using Tessa.Platform;
using Unity;
namespace Tessa.Extensions.Server.Numbers
{
[Registrator]
public sealed class Registrator : RegistratorBase
{
public override void RegisterUnity()
{
this.UnityContainer
.RegisterSingleton<AbIncomingNumberDirector>()
.RegisterSingleton<AbReserveNumberForIncomingRequestExtension>();
}
public override void RegisterExtensions(IExtensionContainer extensionContainer)
{
extensionContainer
.RegisterExtension<ICardRequestExtension, AbReserveNumberForIncomingRequestExtension>(x => x
.WithOrder(ExtensionStage.AfterPlatform)
.WithUnity(this.UnityContainer)
.WhenRequestTypes(AbNumberEventTypes.ReserveNumberForIncoming.ID));
}
public override void FinalizeRegistration()
{
NumberEventTypeRegistry.Instance.Register(AbNumberEventTypes.ReserveNumberForIncoming);
this.UnityContainer
.TryResolve<INumberDirectorContainer>()
?.Register(
typeID: DefaultCardTypes.IncomingTypeID,
getDirectorFunc: c => c.Resolve<AbIncomingNumberDirector>());
}
}
}
В классе выполняются следующие регистрации:
RegisterUnity
- регистрирует классыAbIncomingNumberDirector
иAbReserveNumberForIncomingRequestExtension
в DI-контейнере как синглтоны, чтобы зависимости в их конструкторах также запрашивались из DI-контейнера.RegisterExtensions
- регистрирует запросAbReserveNumberForIncomingRequestExtension
для выполнения черезcardRequest
с указанным типом запроса, равным идентификатору типа событияAbNumberEventTypes.ReserveNumberForIncoming
.FinalizeRegistration
- регистрирует тип событияAbNumberEventTypes.ReserveNumberForIncoming
, а также регистрируетAbIncomingNumberDirector
для обработки событий API нумерации в карточках типаDefaultCardTypes.IncomingTypeID
(т.е. для входящий документов).
Реализация со стороны web-клиента¶
Клиентские объекты и средства их регистрации расположены в папке WebClient SDK\src\solution
. Создадим подпапку numbers
, где будут располагаться все последующие файлы.
Добавим файл abNumberEventTypes.ts
, где определяется идентификатор события.
Important
Это должен быть тот же идентификатор, что и в серверном классе AbNumberEventTypes
.
export namespace AbNumberEventTypes {
export const reserveNumberForIncoming = '3943b50f-d0d8-45ba-b595-f44f24166c81';
}
Добавим файл abNumberDirector.ts
, в нём указаны зависимости для выполнения запроса к серверу по событию API нумерации.
import { IStorage, StorageHelper, TypedField } from '@tessa/core';
import { createInjectToken, inject, injectable } from '@tessa/application';
import { CardRequest, CardResponse, ICardService, ICardService$ } from '@tessa/platform';
import {
NumberContext,
NumberContextActionKeys,
NumberLocationTypes,
NumberObject,
getNumberTypeById
} from 'tessa/cards/numbers';
import { IUIContext } from 'tessa/ui';
import { AbNumberEventTypes } from './abNumberEventTypes';
export interface IAbNumberDirector {
reserveNumberForIncoming(
context: NumberContext,
executeInContext: (action: (context: IUIContext) => void) => void,
modifyRequest?: (request: CardRequest) => void
): Promise<boolean>;
}
/** @category injects */
export const IAbNumberDirector$ = createInjectToken<IAbNumberDirector>('IAbNumberDirector');
@injectable()
export class AbNumberDirector implements IAbNumberDirector {
constructor(@inject(ICardService$) protected readonly _cardService: ICardService) {}
async reserveNumberForIncoming(
context: NumberContext,
executeInContext: (action: (context: IUIContext) => void) => void | Promise<void>,
modifyRequest?: (request: CardRequest) => void
): Promise<boolean> {
// отправляем запрос на сервер для освобождения предыдущего и резервирования нового номера;
// тип запроса и тип события номеров имеют одинаковый идентификатор
return await this.processControlRequest(
context,
AbNumberEventTypes.reserveNumberForIncoming,
executeInContext,
async (ctx, response) => {
const items: IStorage[] = response.NumberQueue?.Items;
if (Array.isArray(items) && items.length !== 0) {
const card = ctx.card;
let cardNumberQueue = StorageHelper.tryGet<IStorage>(card.info, '.numberQueue');
if (!cardNumberQueue) {
cardNumberQueue = {};
card.info['.numberQueue'] = cardNumberQueue;
}
const cardNumberQueueItems = StorageHelper.tryGet<IStorage[]>(cardNumberQueue, 'Items');
if (!Array.isArray(cardNumberQueueItems)) {
cardNumberQueue['Items'] = items;
} else {
cardNumberQueue['Items'] = cardNumberQueueItems.concat(items);
}
}
if (StorageHelper.tryGet(response['Info'], 'Released')) {
await ctx.executeNumberAction(NumberContextActionKeys.Released, ctx.numberObject!);
}
// здесь мы получим номер, зарезервированный на сервере
let reservedNumber: NumberObject | null = null;
const numberObjectStorage = response['NumberObject'];
if (numberObjectStorage) {
const typeStorage = numberObjectStorage['Type'];
if (typeStorage) {
reservedNumber = new NumberObject(
TypedField.tryGet<string>(numberObjectStorage['FullNumber']),
TypedField.tryGet<number>(numberObjectStorage['Number']),
TypedField.tryGet<string>(numberObjectStorage['SequenceName']),
getNumberTypeById(TypedField.get<string>(typeStorage['TypeID']))!
);
}
}
if (!reservedNumber || reservedNumber.isEmpty()) {
return false;
}
// номер непустой, значит он был успешно зарезервирован
await ctx.executeNumberAction(NumberContextActionKeys.Reserved, reservedNumber);
ctx.storeNumber(reservedNumber);
return true;
},
modifyRequest
);
}
private getCardRequest(
context: NumberContext,
type: string,
modifyRequest?: (request: CardRequest) => void
): CardRequest {
const card = context.card;
const numberObject = context.numberObject!;
const numberLocation = context.numberLocation!;
const request = new CardRequest();
request.requestType = type;
request.setCardInfo(card);
const controlName = StorageHelper.tryGet<string>(context.info, '.controlName');
request.info = {
'.request': {
ControlName: controlName ? TypedField.createString(controlName) : null,
StoreMode: TypedField.createInt(card.storeMode),
ControlLocation: {
TypeID: TypedField.createGuid(NumberLocationTypes.Card),
TypeName: TypedField.createString('Card'),
Info: {
'.section': TypedField.createString(numberLocation.section),
'.fullNumberField': TypedField.createString(numberLocation.fullNumberFieldName),
'.numberField': TypedField.createString(numberLocation.numberFieldName),
'.sequenceNameField': numberLocation.sequenceFieldName
? TypedField.createString(numberLocation.sequenceFieldName)
: null
}
},
NumberObject: {
FullNumber:
numberObject.fullNumber != null
? TypedField.createString(numberObject.fullNumber)
: null,
Number: numberObject.num != null ? TypedField.createLong(numberObject.num) : null,
SequenceName:
numberObject.sequenceName != null
? TypedField.createString(numberObject.sequenceName)
: null,
Type: {
TypeID: TypedField.createGuid(numberObject.numberType.id),
TypeName: TypedField.createString(numberObject.numberType.name),
Info: null
},
Info: null
}
}
};
if (modifyRequest) {
modifyRequest(request);
}
return request;
}
private async processControlRequest(
context: NumberContext,
requestType: string,
executeInContext: (action: (context: IUIContext) => void) => void | Promise<void>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
processFunc: (ctx: NumberContext, response: IStorage<any>) => Promise<boolean>,
modifyRequest?: (request: CardRequest) => void
): Promise<boolean> {
const request = this.getCardRequest(context, requestType, modifyRequest);
let response!: CardResponse;
await executeInContext(async () => (response = await this._cardService.request(request)));
const result = response.validationResult.build();
context.validationResult.add(result);
if (!result.isSuccessful) {
return false;
}
const responseInfo = StorageHelper.tryGet<IStorage>(response.tryGetInfo(), '.response');
if (!responseInfo) {
return false;
}
return await processFunc(context, responseInfo);
}
}
Рассмотрим объявления в этом файле:
IAbNumberDirector
- интерфейс, посредством которого будет выполняться запрос в функцииreserveNumberForIncoming
. Функция получаетcontext
- контекст с информацией по номеру на клиенте (NumberContext
),executeInContext
- функция для вызова запроса в контексте карточки (UIContext
),modifyRequest
- функция для изменения запросаcardRequest
перед отправкой на сервер (с её помощью к запросу будет добавлен идентификатор подразделения).IAbNumberDirector$
- токен для встраивания интерфейсаIAbNumberDirector
в DI-контейнер web-клиента.AbNumberDirector
- класс для реализации логики по отправке и получению запроса, объявленного в интерфейсеIAbNumberDirector
. Через конструктор из DI-контейнера запрашиваетсяICardService
- интерфейс API карточек, используемый для отправки запросовcardRequest
. Перечислим функции класса:reserveNumberForIncoming
- функция для отправки запроса с типомAbNumberEventTypes.reserveNumberForIncoming
. Вызывает функциюprocessControlRequest
для отправки запроса этого типа. В её параметреprocessFunc
передаётся лямбда-выражение для обработки ответа на запрос, где: обрабатывается очередь номеровNumberQueue
, полученная от сервера, чтобы объединить её с очередью в карточке; обрабатывается флагReleased
, установленный серверным методомAbIncomingNumberDirector.OnReserveNumberForIncomingAsync
, чтобы уведомить контрол “Нумератор” об освобождении номера (вызов функцииctx.executeNumberAction
); создаётся объект зарезервированного номераreservedNumber
, полученный от сервера, о чём уведомляется контрол “Нумератор”, и этот номер записывается в поля карточки вызовом функцииctx.storeNumber(reservedNumber)
;getCardRequest
- вспомогательная функция, подготавливающая объектCardRequest
для отправки запроса. Выполняет функциюmodifyRequest
, полученную из вызывающего интерфейс кода, чтобы заполнить в запросе дополнительную информацию;processControlRequest
- вспомогательная функция, выполняющая запросcardRequest
в контексте карточки (параметрexecuteInContext
). В случае успешности запроса запускает обработку результата посредством функции из параметраprocessFunc
.
Добавим файл abNumberUIHelper.ts
, в котором объявляется функция createNumberContext
, создающая объект NumberContext
в соответствии с текущим состоянием переданного контрола “Нумератор”.
import { StorageHelper } from '@tessa/core';
import { NumberContext, NumberLocation, NumberObject } from 'tessa/cards/numbers';
import { ICardModel } from 'tessa/ui/cards';
import { NumeratorViewModel } from 'tessa/ui/cards/controls';
export namespace AbNumberUIHelper {
export function createNumberContext(
cardModel: ICardModel,
numeratorControl: NumeratorViewModel
): NumberContext {
const numberObject = new NumberObject(
numeratorControl.fullNumber,
numeratorControl.number,
numeratorControl.sequence,
numeratorControl.numberType
);
const settings = numeratorControl.cardTypeControl.controlSettings;
const sectionId = StorageHelper.tryGet<string>(settings, 'SectionID')!;
const metadataSection = cardModel.cardMetadata.sections.getSectionById(sectionId)!;
const sectionName = metadataSection.name;
const fullNameColumnId = StorageHelper.tryGet<string>(settings, 'FullNumberColumnID')!;
const fullNumberFieldName = metadataSection.columns.getColumnById(fullNameColumnId)!.name!;
const numberColumnId = StorageHelper.tryGet<string>(settings, 'NumberColumnID')!;
const numberFieldName = metadataSection.columns.getColumnById(numberColumnId)!.name!;
const sequenceColumnId = StorageHelper.tryGet<string>(settings, 'SequenceColumnID')!;
const sequenceFieldName = sequenceColumnId
? metadataSection.columns.getColumnById(sequenceColumnId)!.name!
: null;
const numberLocation = new NumberLocation(
sectionName,
fullNumberFieldName,
numberFieldName,
sequenceFieldName
);
const numberContext = new NumberContext(
cardModel.card,
cardModel.cardType,
numberObject,
numberLocation,
cardModel.info
);
if (numeratorControl.name) {
numberContext.info['.controlName'] = numeratorControl.name;
}
return numberContext;
}
}
Созданный функцией контекст NumberContext
включает в себя информацию по номеру из полей карточки и по самой карточке с её типом (требуется для расширяемости, не передаётся в запросе cardRequest
на сервер, кроме идентификаторов карточки и её типа).
Добавим файл abIncomingUIExtension.ts
, в котором расширение-наследник CardUIExtension
для типа карточки “Входящий” будет реализовывать клиентскую бизнес-логику.
import { DisposeList, TypedField } from '@tessa/core';
import { extension, inject } from '@tessa/application';
import { CardStoreMode } from '@tessa/platform';
import { CardUIExtension, ICardUIExtensionContext } from 'tessa/ui/cards';
import { AutoCompleteEntryViewModel, NumeratorViewModel } from 'tessa/ui/cards/controls';
import { IAbNumberDirector, IAbNumberDirector$ } from './abNumberDirector';
import { AbNumberUIHelper } from './abNumberUIHelper';
@extension({ name: 'AbIncomingUIExtension' })
export class AbIncomingUIExtension extends CardUIExtension {
private readonly _disposeList = new DisposeList();
constructor(@inject(IAbNumberDirector$) private readonly _abNumberDirector: IAbNumberDirector) {
super();
}
async initialized(context: ICardUIExtensionContext): Promise<void> {
if (context.card.storeMode !== CardStoreMode.Insert) {
return;
}
const cardModel = context.model;
const numeratorControl = cardModel.controlsBag.find(
x => x instanceof NumeratorViewModel
) as NumeratorViewModel;
const departmentControl = cardModel.controlsBag.find(
x => x instanceof AutoCompleteEntryViewModel && x.view?.metadata?.alias === 'Departments'
) as AutoCompleteEntryViewModel;
if (numeratorControl && departmentControl) {
this._disposeList.add(
departmentControl.valueSet.addWithDispose(async e => {
// e.fields содержит ячейки выбранной строки представления (по алиасам колонок);
// RoleID содержит ссылку на подразделение
const departmentId = e.fields?.get('RoleID');
if (departmentId) {
await this._abNumberDirector.reserveNumberForIncoming(
AbNumberUIHelper.createNumberContext(cardModel, numeratorControl),
cardModel.executeInContext,
cardRequest => {
cardRequest.info['DepartmentID'] = TypedField.createGuid(departmentId);
}
);
}
})!
);
}
}
finalized(): void {
this._disposeList.dispose();
}
}
Опишем объявления в этом файле:
- Класс расширения
AbIncomingUIExtension
получает через конструктор зависимостьIAbNumberDirector
, позволяющую отправить запрос к серверному API нумерации. - Функция
initialized
срабатывает при каждом открытии/рефреше карточки, когда построены вью модели всех контролов, и выполняет следующие действия:- согласно постановке задачи, действия выполняются только для созданных, но ещё несохранённых карточек, у которых свойство
card.storeMode
равноCardStoreMode.Insert
. Если потребуется также выполнять логику для уже сохранённых карточек, то проверку следует убрать здесь и в серверном методеAbIncomingNumberDirector.IsAvailableCoreAsync
; - выполняется поиск вью моделей контрола “Нумератор” и контрола “Ссылка” для выбора подразделений. В проектном решении рекомендуется указать контролам алиасы, по которым можно получить их в расширении, но для работы примера на типовой карточке входящего документа поиск выполняется без алиаса;
- если оба контрола найдены, то для контрола “Ссылка” добавляется событие
valueSet
, вызываемое при каждом изменении значения с подтверждением (либо после нажатия Enter при вводе с клавиатуры, либо после выбора по клику в выпадающем списке, либо через кнопку с троеточием); - в событии получаем поле
RoleID
из строки представленияe.Fields
, что для представления с алиасомDepartments
соответствует идентификатору подразделения. Если идентификатор не пустой, то вызываем функциюreserveNumberForIncoming
для объектаIAbNumberDirector
, передавая ей контекст нумерацииNumberContext
(функцияAbNumberUIHelper.createNumberContext
, получающая вью модель контрола “Нумератор”), функцию для выполнения запроса в контексте карточкиcardModel.executeInContext
и лямбда-выражение, которое добавляет в запросcardRequest
по ключуDepartmentID
идентификатор выбранного подразделения (используется в серверном методеAbIncomingNumberDirector.TryGetSequenceNameCoreAsync
).
- согласно постановке задачи, действия выполняются только для созданных, но ещё несохранённых карточек, у которых свойство
- Функция
finalized
срабатывает при освобождении открытой ранее карточки (в т.ч. при рефреше карточки или при закрытии вкладки), где освобождается списокDisposeList
, что приводит к отписке от событияdepartmentControl.valueSet
. Рекомендуется всегда отписываться от событий во избежание утечек памяти.
Добавим файл registrator.ts
, содержащий регистрацию описанных объектов.
import { ExtensionRegistrator, ExtensionStage } from '@tessa/application';
import { AbNumberDirector, IAbNumberDirector$ } from './abNumberDirector';
import { AbIncomingUIExtension } from './abIncomingUIExtension';
import { whenCardTypeNameIs } from '@tessa/platform';
export const AbNumbersRegistrator: ExtensionRegistrator = {
async registerTypes(container) {
container.bind(IAbNumberDirector$).to(AbNumberDirector).inSingletonScope();
},
async registerExtensions(container) {
container.registerExtension({
extension: AbIncomingUIExtension,
stage: ExtensionStage.AfterPlatform,
when: whenCardTypeNameIs('Incoming')
});
}
};
Рассмотрим выполняемые действия:
- В DI-контейнере регистрируется класс
AbNumberDirector
как синглтон, доступный по интерфейсуIAbNumberDirector
. - Регистрируется расширение
AbIncomingUIExtension
для типа карточки с алиасомIncoming
(т.е. для входящих документов).
Теперь необходимо добавить вызов класса регистратора AbNumbersRegistrator
в файле WebClient SDK\src\solution\bundleRegistrator.ts
.
import { Application } from 'tessa/application';
import { AbNumbersRegistrator } from './numbers/registrator';
Application.instance.registerBundle({
name: 'Tessa.Extensions.Solution.js',
buildTime: process.env.BUILD_TIME!,
registry: [
AbNumbersRegistrator
]
});
Note
В этом файле перечисляются все подключённые регистраторы, поэтому помимо AbNumbersRegistrator
будут присутствовать и другие классы регистраторов из проектного решения.
Проверка работоспособности примера¶
Выполним сборку серверных расширений (файл Tessa.Extensions.Server.dll
) и расширений web-клиента (результат сборки из папки WebClient SDK
). Скопируем расширения в папку веб-сервиса web
.
Теперь откроем web-клиент, создадим в нём карточку с типом “Входящий”.
- Пока карточка не сохранена, при каждом изменении ссылки в поле “Подразделение” будет отображаться новый зарезервированный номер, причём для одного и того же подразделения номера идут последовательно.
- При открытии окна структуры (Ctrl+G) по ключу
.numberQueue
видна клиентская очередь номеров, которая изменяется при выборе новой ссылки на подразделение. - При сохранении карточки отображаемый номер переходит из зарезервированного состояния в выделенный, а все прочие номера дерезервируются.
- Если закрыть вкладку с карточкой без сохранения, то все зарезервированные номера дерезервируются, т.е. при следующем создании карточки они могут быть зарезервированы вновь.