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

Запросы к 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); } } }

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

  1. NotifyOnReserveNumberForIncomingAsync - “точка входа” для обработки события, этот метод вызывается из запроса AbReserveNumberForIncomingRequestExtension.
  2. OnReserveNumberForIncomingAsync - алгоритм обработки события. Здесь освобождается предыдущий номер (пришедший с клиента) вызовом ReleaseAndCommitAtServerAsync и резервируется новый номер вызовом ReserveAndCommitAtServerAsync.
    • В процессе наполняется очередь нумерации NumberQueue, которая будет автоматически отправлена на клиент.
    • Флаг Released отправляется на клиент при успешном освобождении предыдущего номера.
    • Алгоритм этого метода аналогичен стандартному резервированию номера через контрол “Нумератор”, что соответствует серверной части метода DocumentNumberDirector.OnReservingNumberFromControlAsync.
  3. BeforeReserveNumberForIncomingAsync - метод, запускаемый перед вызовом OnReserveNumberForIncomingAsync, в котором возможно выполнить подготовительные действия перед обработкой события. В текущем примере он не используется, но его можно переопределить для наследников AbIncomingNumberDirector, если в проекте определяются различные реализации INumberDirector для разных типов карточек.
  4. IsAvailableCoreAsync - функция, определяющая доступность события для текущего состояния карточки в соответствии с настройками типа карточки/документа.
    • Здесь указано, что событие ReserveNumberForIncoming доступно только в серверном контексте для ещё несохранённых карточек, и при условии, что в запросе cardRequest передан идентификатор выбранного подразделения в info['DepartmentID'].
    • Если функция возвращает false, то методы BeforeReserveNumberForIncomingAsync и OnReserveNumberForIncomingAsync не будут выполнены, и клиент получит информацию, что запрос неуспешен.
  5. 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>()); } } }

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

  1. RegisterUnity - регистрирует классы AbIncomingNumberDirector и AbReserveNumberForIncomingRequestExtension в DI-контейнере как синглтоны, чтобы зависимости в их конструкторах также запрашивались из DI-контейнера.
  2. RegisterExtensions - регистрирует запрос AbReserveNumberForIncomingRequestExtension для выполнения через cardRequest с указанным типом запроса, равным идентификатору типа события AbNumberEventTypes.ReserveNumberForIncoming.
  3. 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); } }

Рассмотрим объявления в этом файле:

  1. IAbNumberDirector - интерфейс, посредством которого будет выполняться запрос в функции reserveNumberForIncoming. Функция получает context - контекст с информацией по номеру на клиенте (NumberContext), executeInContext - функция для вызова запроса в контексте карточки (UIContext), modifyRequest - функция для изменения запроса cardRequest перед отправкой на сервер (с её помощью к запросу будет добавлен идентификатор подразделения).
  2. IAbNumberDirector$ - токен для встраивания интерфейса IAbNumberDirector в DI-контейнер web-клиента.
  3. 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(); } }

Опишем объявления в этом файле:

  1. Класс расширения AbIncomingUIExtension получает через конструктор зависимость IAbNumberDirector, позволяющую отправить запрос к серверному API нумерации.
  2. Функция initialized срабатывает при каждом открытии/рефреше карточки, когда построены вью модели всех контролов, и выполняет следующие действия:
    • согласно постановке задачи, действия выполняются только для созданных, но ещё несохранённых карточек, у которых свойство card.storeMode равно CardStoreMode.Insert. Если потребуется также выполнять логику для уже сохранённых карточек, то проверку следует убрать здесь и в серверном методе AbIncomingNumberDirector.IsAvailableCoreAsync;
    • выполняется поиск вью моделей контрола “Нумератор” и контрола “Ссылка” для выбора подразделений. В проектном решении рекомендуется указать контролам алиасы, по которым можно получить их в расширении, но для работы примера на типовой карточке входящего документа поиск выполняется без алиаса;
    • если оба контрола найдены, то для контрола “Ссылка” добавляется событие valueSet, вызываемое при каждом изменении значения с подтверждением (либо после нажатия Enter при вводе с клавиатуры, либо после выбора по клику в выпадающем списке, либо через кнопку с троеточием);
    • в событии получаем поле RoleID из строки представления e.Fields, что для представления с алиасом Departments соответствует идентификатору подразделения. Если идентификатор не пустой, то вызываем функцию reserveNumberForIncoming для объекта IAbNumberDirector, передавая ей контекст нумерации NumberContext (функция AbNumberUIHelper.createNumberContext, получающая вью модель контрола “Нумератор”), функцию для выполнения запроса в контексте карточки cardModel.executeInContext и лямбда-выражение, которое добавляет в запрос cardRequest по ключу DepartmentID идентификатор выбранного подразделения (используется в серверном методе AbIncomingNumberDirector.TryGetSequenceNameCoreAsync).
  3. Функция 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') }); } };

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

  1. В DI-контейнере регистрируется класс AbNumberDirector как синглтон, доступный по интерфейсу IAbNumberDirector.
  2. Регистрируется расширение 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 видна клиентская очередь номеров, которая изменяется при выборе новой ссылки на подразделение.
  • При сохранении карточки отображаемый номер переходит из зарезервированного состояния в выделенный, а все прочие номера дерезервируются.
  • Если закрыть вкладку с карточкой без сохранения, то все зарезервированные номера дерезервируются, т.е. при следующем создании карточки они могут быть зарезервированы вновь.
Back to top