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

Создание настольного (desktop) приложения, использующего API TESSA

В этом примере мы рассмотрим создание настольного WPF приложения .NET 5, являющегося клиентом СЭД TESSA. Оно может быть использовано в качестве основы для создания ваших приложений, которым нужно взаимодействовать с TESSA, например: отображать и редактировать карточки, отображать представления и другие подобные задачи.

Рекомендуется при перовом ознакомлении прочитать статью линейно от начала до конца без пропуска тех или иных разделов, поскольку информация подробно изложенная в вышестоящих разделах используется в нижеследующих без подробного разъяснения. В дальнейшем, вы можете пользоваться данной статьёй как справочником, перемещаясь непосредственно к нужному разделу.

Макет

Рассмотрим макет реализуемого нами приложения.

Приложение будет состоять из двух областей:

  • левая область - представление со списком карточек (условно, область навигации);
  • правая область - редактор выбранной в левой области карточки.

Заголовок приложения также будет настраиваться. В него будет выводиться название приложения (или заданная строка) и версия платформы TESSA, которая была использована в данном приложении.

Создание проекта

Начнём с создания проекта приложения.

В Visual Studio откройте файл решения Source\Tessa.Extensions.sln. Создайте папку Applications при помощи соответствующего пункта контекстного меню Solution Explorer.

Далее создайте проект WPF Application, задав ему имя TessaDesktopApp и выбрав в качестве среды выполнения .NET 5.0.

Важно! Обратите внимание, что в местоположении нужно указать папку Applications.

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

На этом этапе у Вас есть макет приложения WPF. Перейдём к его настройке для использования в качестве простейшего клиента к СЭД TESSA.

Настройка файла проекта

Откройте для редактирования файл проекта .csproj и измените его содержимое следующим образом:

<Project Sdk="Microsoft.NET.Sdk.Web">

<Import Project="$(ProjectDir)../../Tessa.targets" /> <Import Project="$(ProjectDir)../../Tessa.Extensions.targets" /> <Import Project="$(ProjectDir)../../Tessa.Runtime.targets" />

<PropertyGroup> <OutputType>WinExe</OutputType> <TargetFramework>net5.0-windows</TargetFramework> <UseWPF>true</UseWPF> <LangVersion>latest</LangVersion> <RestoreSources>$(RestoreSources);../../Bin/packages;https://api.nuget.org/v3/index.json</RestoreSources> </PropertyGroup>

</Project>

Note

Свойство <RestoreSources>$(RestoreSources);../../Bin/packages;</RestoreSources> необходимо для того, чтобы иметь возможность работать с локальными копиями NuGet-пакетов. Это особенно полезно при тестировании новых возможностей.

Для реализации возможности нашего приложения выступать клиентом СЭД TESSA в необходимо добавить зависимость от NuGet пакета Tessa.UI.

Перейдите в диалог выбора NuGet-пакетов, для этого в контекстном меню на узле проекта TessaDesktopApp в панели инструментов Solution Explorer выберите пункт Manage NuGet Packages.

Перейдите на вкладку Browse и установите пакет Tessa.UI, выбрав версию соответствующую версии вашей сборки TESSA. Например, для версии сборки TESSA 3.6.0 с патчем 5 укажите версию пакетов также 3.6.0.5. Не обновляйте пакеты до более новых версий до того, как будет обновлена инсталляция всей платформы (вместе с конфигурацией, базой данных и расширениями).

Note

Обратите внимание, что пример из данного руководства будет работать только начиная с версии TESSA 3.6.0.5.

Добавление конфигурационных файлов

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

  • файлы настройки самого приложения;
  • файл настройки поиска динамических зависимостей;
  • файл настройки инструмента протоколирования.

Добавление файлов настройки приложения

К файлам настройки TESSA относятся файлы app*.json. В нашем приложении будет использовано два таких файла.

Добавьте в проект новый файл app.json следующего содержания:

{ ".include": [ "app-*.json" ],

"Settings": { "BaseAddress": "https://localhost/tessa", "SkipWinAuth": false, "OpenTimeout": "00:01:01", "CloseTimeout": "00:01:02", "SendTimeout": "00:40:00",

"Logo": "", "LogoText": "", "LogoHideVersion": false, "Title": "Tessa Desktop App custom title", "ProbingPath": "extensions", "TitleDescription": "my amazing App!",

"//ProxyUri": "http://127.0.0.1:8888", "//ProxyClass": "Tessa.UI.LoginDialogProxy, Tessa.UI" } }

Это основной файл настроек приложения (здесь дать ссылку на раздел документации, с удивлением обнаружил, что такой раздел отсутствует!). Благодаря настройке .include настройки, указанные в этом файле, могут быть переопределены настройками включаемых файлов, маска поиска которых указана в данном параметре.

Укажите в свойствах данного файла действие Copy to Output Directory - Copy if never.

Теперь добавим ссылку на файл клиентских настроек app-local-client.json, автоматически создаваемый в проектном решении. Для этого из контекстного меню узла проекта TessaDesktopApp в Solution Explorer выберете команду Add | Existing Item.... В открывшемся диалоге перейдите в корневую папку Source, выберите в ней файл app-local-client.json и нажмите на кнопку Add as Link.

В свойствах данного файла также укажите действие Copy to Output Directory - Copy if never, и задайте значение свойству Build Action - None.

Добавление файла настройки поиска динамических зависимостей

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

Создайте в проекте новую папку extensions. В ней создайте новый файл extensions.xml следующего содержания:

<?xml version="1.0" encoding="utf-8" ?> <extensions xmlns="http://syntellect.ru/tessa/include">

<include file="Tessa.Extensions.Default.Shared.dll" /> <include file="Tessa.Extensions.Default.Client.dll" clientOnly="true" />

<include file="Tessa.Extensions.Shared.dll" /> <include file="Tessa.Extensions.Client.dll" clientOnly="true" />

</extensions>

Укажите в свойствах данного файла действие Copy to Output Directory - Copy if never.

Добавление файла настройки инструмента протоколирования

В файле настроек инструмента протоколирования содержатся настройки, отвечающие за полноту вывода протокольной информации при помощи NLog.

Создайте в проекте файл NLog.config следующего содержания:

<?xml version="1.0" encoding="utf-8" ?> <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

<include file="nlog-*.config" ignoreErrors="true"/>

<targets async="true"> <target name="file" xsi:type="File" encoding="utf-8" writeBom="true" fileName="${basedir}/log.txt" /> <target name="null" xsi:type="Null" formatMessage="false" /> </targets>

<rules> <logger name="*" minlevel="Trace" writeTo="file" /> </rules>

</nlog>

Укажите в свойствах данного файла действие Copy to Output Directory - Copy if never.

Реализация основной функциональности

При реализации нашего приложения мы будем использовать паттерн MVVM (Model-View-ViewModel).

Создание гибко настраиваемого представления

Начнём реализацию с создания представления. Для большей гибкости и возможностей его настройки под нужды конечного пользователя создадим его в TessaAdmin при помощи собственного типа диалога.

Перейдите в раздел “Карточки” и создайте новый тип диалога.

Укажите в качестве имени AbUserInterface, в качестве заголовка “Пользовательский интерфейс представления”, в качестве группы AbTessaDesktopApp. Здесь и далее мы условимся использовать проектный префикс Ab.

В данном примере мы не будем использовать виртуальную схему (подробнее о ней).

Перейдите в раздел Вкладки, задайте для уже созданной вкладки название View и заголовок “Вкладка представления”. Далее добавьте на форму Колоночный блок, установите у него флаги Растягивать по вертикали и Скрывать заголовок. В колоночный блок добавьте элемент управления Представление, задайте ему заголовок “Представление”, алиас ViewControl и алиас представления Cars.

Не забудьте сохранить сделанные изменения.

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

Преимущество такого подхода заключается в том, что при необходимости вы можете в любой момент изменить представление в программе TessaAdmin без необходимости изменять код клиентского приложения. Именно так и работает приложение TessaClient.

Второй важный момент, на который следует обратить внимание, в том, что все элементы управления и формы, создаваемые в TessaAdmin, уже используют паттерн MVVM, поэтому мы можем задействовать их “из коробки”, как будет показано далее.

Реализация главной модели представления

Создайте в проекте новую папку ViewModels, в ней создайте класс MainViewModel.cs следующего содержания:

using System; using System.Threading; using System.Threading.Tasks; using Tessa.Platform; using Tessa.UI; using Tessa.UI.Cards; using Tessa.UI.Cards.Controls; using Tessa.UI.Views.Content; using Tessa.Views.Metadata;

namespace TessaDesktopApp.ViewModels { /// <summary> /// Главная модель представления. /// </summary> public sealed class MainViewModel : ViewModel<EmptyModel>, IAsyncInitializable { #region Fields

/// <summary> /// Делегат создания модели типа диалога. /// </summary> private readonly CreateDialogFormFuncAsync createDialogFormFuncAsync;

/// <summary> /// Функция-помощник создания редактора. /// </summary> private readonly Func<ICardEditorModel> createEditorFunc;

/// <summary> /// Модель представления списка доступных карточек. /// </summary> private IFormViewModel viewFormViewModel;

/// <summary> /// Модель представления для элемента управления "Представление". /// </summary> private CardViewControlViewModel viewControlViewModel;

/// <summary> /// Модель представления редактора карточек. /// </summary> private ICardEditorModel cardEditorViewModel;

/// <summary> /// Псевдоним колонки с идентификатором карточки. /// </summary> private string cardIdName;

#endregion

#region Constructors

/// <summary> /// Конструктор главной модели представления с разрешением зависимостей из Unity. /// </summary> /// <param name="createDialogFormFuncAsync">Делегат создания модели типа диалога.</param> /// <param name="createEditorFunc">Функция создания редактора карточек.</param> public MainViewModel(CreateDialogFormFuncAsync createDialogFormFuncAsync, Func<ICardEditorModel> createEditorFunc) { this.createDialogFormFuncAsync = createDialogFormFuncAsync; this.createEditorFunc = createEditorFunc; }

#endregion

#region ViewModel Properties

/// <summary> /// Модель представления списка доступных карточек. /// </summary> public IFormViewModel ViewFormViewModel => this.viewFormViewModel;

/// <summary> /// Модель представления редактора карточек. /// </summary> public ICardEditorModel CardEditorViewModel => this.cardEditorViewModel;

#endregion

#region IAsyncInitializable

/// <inheritdoc/> async ValueTask IAsyncInitializable.InitializeAsync(CancellationToken cancellationToken) { // получаем модель представления со списком доступных карточек ICardModel cardModel; (this.viewFormViewModel, cardModel) = await this.createDialogFormFuncAsync( "AbUserInterface", "View", cancellationToken: cancellationToken);

// получаем и настраиваем редактор карточки this.cardEditorViewModel = this.createEditorFunc(); this.cardEditorViewModel.DialogName = CardUIHelper.DefaultDialogName; // настраиваем обработчик для получения сообщений об изменении выделенной строки this.viewControlViewModel = cardModel.Controls.TryGet<CardViewControlViewModel>("ViewControl"); if (this.viewControlViewModel is null) { return; } this.cardIdName = TryGetCardIDName(this.viewControlViewModel.ViewMetadata); this.viewControlViewModel.Table.RowSelected += async (o, e) => { await this.SelectViewRowAsync(e.Row); };

await this.SelectViewRowAsync(this.viewControlViewModel.Table.SelectedItem, cancellationToken); }

#endregion

#region Internal Methods

/// <summary> /// Выполняет попытку открыть карточку с заданным идентификатором. /// </summary> /// <param name="cardID">Идентификатор карточки.</param> internal void TrySelectCard(Guid cardID) { var table = this.viewControlViewModel?.Table; if (table is null || string.IsNullOrEmpty(this.cardIdName)) { return; } foreach (var row in table.Items) { if (row.Data[this.cardIdName] is not Guid ID) { continue; } if (ID == cardID) { table.SelectedItem = row; break; } } }

#endregion

#region Private methods

/// <summary> /// Обработчик события выделения элемента в представлении. /// </summary> /// <param name="row">Выделенная строка.</param> /// <param name="cancellationToken">Объект для отмены асинхронной операции.</param> /// <returns>Асинхронная задача.</returns> private async Task SelectViewRowAsync( TableRowViewModel row, CancellationToken cancellationToken = default) { // нет идентификатора карточки, синхронизация невозможна if (string.IsNullOrEmpty(this.cardIdName) || row is null) { return; } if (row.Data[this.cardIdName] is not Guid cardID || cardID == Guid.Empty) { return; }

// открываем выделенную карточку await using (UIContext.Create(this.cardEditorViewModel.Context)) { await this.cardEditorViewModel.OpenCardAsync(cardID, null, null, context: this.cardEditorViewModel.Context, cancellationToken: cancellationToken); } }

/// <summary> /// Помощник получения псевдонима колонки с идентификатором карточки из представления. /// </summary> /// <param name="viewMetadata">Метаданные представления.</param> /// <returns>Возвращает строку с именем колонки идентификатора карточки, или <c>null</c>, если её не удалось найти.</returns> private static string TryGetCardIDName(IViewMetadata viewMetadata) { if (viewMetadata is null) { return null; }

foreach (var reference in viewMetadata.References) { if (!reference.IsCard || !reference.OpenOnDoubleClick) { continue; } var column = viewMetadata.Columns.FindByName(reference.ColPrefix + "ID") ?? viewMetadata.Columns.FindByName(reference.ColPrefix + "RowID"); if (column is not null) { return column.Alias; } }

return null; }

#endregion } }

Обратите внимание, что для разрешения зависимостей мы используем механизм Dependency Injection, реализованный в библиотеке Unity.

Реализация главного представления приложения

Создайте в проекте новую папку Views, в ней создайте файл пользовательского элемента управления WPF (User Control (WPF)) MainView.xaml следующего содержания:

<UserControl x:Class="TessaDesktopApp.Views.MainView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:TessaDesktopApp.Views" xmlns:markup="clr-namespace:Tessa.UI.Markup;assembly=Tessa.UI" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" d:DesignHeight="450" d:DesignWidth="800" mc:Ignorable="d"> <Grid Margin="5"> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions>

<ContentPresenter Grid.Column="0" Content="{Binding ViewFormViewModel, Mode=OneTime}" /> <GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Center" VerticalAlignment="Stretch" /> <ContentPresenter Grid.Column="2" Content="{Binding CardEditorViewModel, Mode=OneTime}" /> </Grid> </UserControl>

Создание основного файла ресурсов приложения

Создайте в проекте новую папку Resources, в ней создайте файл словаря ресурсов (Resource Dictionary (WPF)) ViewModels.xaml следующего содержания:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:TessaDesktopApp.ViewModels" xmlns:vw="clr-namespace:TessaDesktopApp.Views">

<DataTemplate DataType="{x:Type vm:MainViewModel}"> <vw:MainView /> </DataTemplate>

</ResourceDictionary>

Регистрация типа приложения в TESSA

Для корректной работы необходимо зарегистрировать приложение в TESSA, поскольку ранее оно не существовало. В TessaAdmin перейдите на вкладку Схема.

Вначале добавим новую библиотеку.

Установим ей имя AbTessaDesktopApp и описание “Библиотека для примера desktop-приложения”.

Далее в дереве перейдите на узел Tessa | System | ApplicationNames | Записи. Добавьте новую строку с идентификатором 038eb23a-1c9a-4c64-a875-312fa28db5e2, именем TessaDesktopApp, в качестве библиотеки укажите только что созданную AbTessaDesktopApp.

Сохраните ваши изменения.

Настройка зависимостей

Внимание, для работы приложения не забудьте скопировать библиотеку Syncfusion.SfChart.WPF.dll из папки приложения TessaClient в папку проекта. Далее добавьте её в проект и укажите в свойствах данного файла действие Copy to Output Directory - Copy if never.

Настройка иконки приложения

Скопируйте иконку TessaClient.ico в папку проекта и переименуйте её в TessaDesktopApp.ico. В свойствах проекта установите данный файл в качестве иконки приложения.

Сохраните изменения, после чего файл иконки должен появиться в проекте. Укажите в его свойствах действие Copy to Output Directory - Copy if never.

Настройка приложения для получения ссылок

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

using System.Windows; using Tessa.Platform.Runtime;

[assembly: ThemeInfo( ResourceDictionaryLocation.None, ResourceDictionaryLocation.SourceAssembly )]

[assembly: Application("tessadesktopapp")]

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

В нашем примере будет в приложении и расширениях будет совместно использоваться класс-помощник, содержащий идентификаторы приложений.

Вначале, создадим его. В проекте Tessa.Extensions.Shared создайте новый класс AbApplicationIdentifiers следующего содержания:

using System;

namespace Tessa.Extensions.Shared { /// <summary> /// Идентификаторы приложений проекта. /// </summary> public static class AbApplicationIdentifiers { #region Public Static Fields

/// <summary> /// Приложение TessaDesktopApp. /// </summary> /// <remarks> /// Идентификатор приложения из таблицы "ApplicationNames". /// </remarks> public static readonly Guid TessaDesktopApp = new Guid("038eb23a-1c9a-4c64-a875-312fa28db5e2");

#endregion } }

Далее добавим его как ссылку в проект TessaDesktopApp. Для этого из контекстного меню узла проекта в Solution Explorer выберете команду Add | Existing Item.... В открывшемся диалоге перейдите в папку проекта Tessa.Extensions.Shared, выберите в ней файл AbApplicationIdentifiers.cs и нажмите на кнопку Add as Link.

Настройка главного класса приложения

Основная часть нашего приложения готова, теперь осталось настроить главный класс, чтобы он получил все возможности, доступные в платформе TESSA “из коробки”, а именно:

  • Работа с указанным в настройках веб-сервисом.
  • Публикация из командной строки с параметром /publish или с использованием команд консольной утилиты tadmin PackageApp и ImportCards (подробнее об этом далее).
  • Запуск из Tessa Applications и обработка ссылок вида tessa://.
  • Использование возможностей стандартного входа пользователя в систему, включая отображение при необходимости окна логина, и автоматическое переоткрытие сессии.
  • Обработка стандартных параметров командной строки.
  • Поддержка клиентских расширений, кроме расширений для кнопок боковых панелей.
  • Возможность работы с метаинформацией, приходящей от сервера, включая этап инициализации, что позволяет написать своё серверное расширение инициализации.
  • Поддержка возможностей локализации.

Для этого необходимо выполнить несколько шагов.

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

<ui:TessaApplication x:Class="TessaDesktopApp.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ui="clr-namespace:Tessa.UI;assembly=Tessa.UI"> <Application.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="pack://application:,,,/Tessa.UI;component/Themes/Generic.xaml" /> <ResourceDictionary Source="pack://application:,,,/Tessa.UI;component/Resources/ViewModels.xaml" /> <ResourceDictionary Source="/Resources/ViewModels.xaml" /> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources> </ui:TessaApplication>

Теперь базовым классом приложения является TessaApplication из библиотеки Tessa.UI, также мы подключили все ресурсы из этой библиотеки, что, как и говорилось ранее, позволит использовать формы и элементы управления TESSA “из коробки”.

Вторая часть изменений касается файла поддержки App.xaml.cs. Здесь мы проводим окончательную настройку приложения, включая контейнер библиотеки Unity для разрешения зависимостей. Замените содержимое файла App.xaml.cs на следующее:

using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using System.Windows; using Tessa.Applications; using Tessa.Applications.Messages; using Tessa.Applications.Services.PlatformApplication; using Tessa.Cards; using Tessa.Extensions; using Tessa.Extensions.Shared; using Tessa.Platform; using Tessa.Platform.Links; using Tessa.Platform.Runtime; using Tessa.Platform.Storage; using Tessa.UI; using Tessa.UI.AppManager.Synchronization; using Tessa.UI.Client; using Tessa.UI.Runtime; using Tessa.UI.Windows; using TessaDesktopApp.ViewModels; using Unity;

namespace TessaDesktopApp { /// <summary> /// Interaction logic for App.xaml /// </summary> public partial class App { #region Fields

/// <summary> /// Шина для обработки сообщений от "AppManager". /// </summary> private IApplicationMessageBus bus;

#endregion

#region Base Overrides

/// <inheritdoc/> protected override async Task OnApplicationStartupAsync(StartupEventArgs e, CancellationToken cancellationToken = default) { await TessaPlatform.InitializeFromConfigurationAsync(cancellationToken: cancellationToken);

// подготовка UnityContainer-а this.UnityContainer = new UnityContainer(); this.UnityContainer .RegisterClient() .RegisterApplicationsOnClient() .RegisterApplicationSynchronization();

ClientRegistrator.ConfigureContainer(this.UnityContainer); this.UnityContainer .RegisterType<MainViewModel>(TypeLifetime.PerResolve) .RegisterApplicationServiceOnClient() .FindAndRegisterExtensionsOnClient(out List<string> actualFoldersList) .FinalizeClientRegistration(actualFoldersList);

var linkManager = this.UnityContainer.Resolve<ILinkManager>(); linkManager.Register(CardHelper.OpenCardLinkAction, this.OpenCardHandlerAsync);

var resourceRegistrator = this.UnityContainer.Resolve<IApplicationResourceRegistrator>(); resourceRegistrator.AddResourcesTo(this);

var iconsRegistrator = this.UnityContainer.Resolve<IApplicationResourceRegistrator>(ApplicationResourceNames.Icons); iconsRegistrator.AddResourcesTo(UIHelper.Icons.MergedDictionaries);

// запускаем приложение this.Application = this.UnityContainer.Resolve<IApplication>();

this.Application.Launched += this.ApplicationLaunched; this.Application.ShuttingDown += this.ApplicationShuttingDown;

// до начала запуска приложения используем значение из конфига ClientTracingHelper.RegisterExtensionTracing(this.UnityContainer);

ApplicationLaunchResult result = await this.Application.LaunchAsync( new ApplicationLaunchParameters( AbApplicationIdentifiers.TessaDesktopApp, ct => this.Dispatcher.InvokeAsync(this.ActivateShell).Task, e.Args) { Splash = this.LauncherSplash, }, cancellationToken).ConfigureAwait(false);

if (result != ApplicationLaunchResult.Launched) { return; }

// создаём и отображаем главное окно Task task = await this.Dispatcher.InvokeAsync(async () => { var viewModel = this.UnityContainer.Resolve<MainViewModel>(); if (viewModel is IAsyncInitializable initializable) { await initializable.InitializeAsync(cancellationToken); // возвращаемся в поток UI }

var session = this.UnityContainer.Resolve<ISession>(); var applicationDescriptor = this.UnityContainer.Resolve<IApplicationDescriptor>(); var configurationManager = this.UnityContainer.Resolve<IConfigurationManager>(); var myTitleDescription = configurationManager.Configuration.Settings.TryGet<string>("TitleDescription");

this.MainWindow = new TessaWindow { Content = viewModel, Title = string.Format("{0} ({1}){2} ({3}) - версия платформы {4} от {5}", applicationDescriptor.Name, session.ServerCode, myTitleDescription is not null ? $", {myTitleDescription}" : string.Empty, applicationDescriptor.ApplicationVersion, BuildInfo.Version, FormattingHelper.FormatDate(BuildInfo.Date, convertToLocal: false)), WindowStartupLocation = WindowStartupLocation.CenterScreen, Icon = ApplicationHelper.ApplicationIcon, };

this.MainWindow.Show(); }).Task.ConfigureAwait(false);

await task.ConfigureAwait(false); }

#endregion

#region Private Methods

/// <summary> /// Обработчик события успешного запуска приложения. /// </summary> /// <param name="sender">Объект, отправитель события.</param> /// <param name="e">Аргументы события события.</param> private async void ApplicationLaunched(object sender, ApplicationContextDeferredEventArgs e) { if (e.Context == null || e.Context.Parameters == null || !e.Context.Parameters.LaunchedByAppManager) { return; }

this.bus = this.UnityContainer.Resolve<IApplicationMessageBus>();

using (e.Defer()) { await this.bus.RegisterAsync(ApplicationApiVersionNames.Get(e.Context.Parameters.AppManagerApi)).ConfigureAwait(false); this.bus.MessageReceived += this.OnMessageReceived; } }

/// <summary> /// Обработчик события завершения приложения. /// </summary> /// <param name="sender">Объект, отправитель события.</param> /// <param name="e">Аргументы события события.</param> private void ApplicationShuttingDown(object sender, ApplicationContextDeferredEventArgs e) { if (e.Context == null || e.Context.Parameters == null || !e.Context.Parameters.LaunchedByAppManager || this.bus == null) { return; }

this.bus.Dispose(); this.bus = null; }

/// <summary> /// Обработчик поступления сообщения от AppManager-а. /// </summary> /// <param name="sender">Объект, отправитель события.</param> /// <param name="e">Аргументы события события.</param> private async void OnMessageReceived(object sender, MessageReceivedEventArgs e) { if (e.Message is ApplicationLinkMessage linkMessage) { e.Handled = true;

var linkManager = this.UnityContainer.Resolve<ILinkManager>(); var messageProvider = this.UnityContainer.Resolve<IMessageProvider>(); Func<CancellationToken, Task> activateShellActionAsync = ct => this.Dispatcher.InvokeAsync(this.ActivateShell).Task;

bool handled = await linkManager.ProcessLinkAsync( linkMessage.LinkArguments, messageProvider, this.UnityContainer, activateShellActionAsync).ConfigureAwait(false);

if (!handled) { // пришло сообщение от AppManager, а приложение не смогло распарсить ссылку - // всё равно отображаем окно приложения await activateShellActionAsync(CancellationToken.None).ConfigureAwait(false); } } }

/// <summary> /// Обработчик события открытия карточки по ссылке от AppManager-а. /// </summary> /// <param name="context">Контекст ссылки на открытие карточки.</param> /// <returns>Асинхронная задача.</returns> private async Task OpenCardHandlerAsync(ILinkContext context) { if (!CardHelper.TryParseLink( context.Parameters, out Guid cardID, out string displayValue, out Guid? fileID, out Guid? versionID)) { return; }

// если окно было свёрнуто - разворачиваем его await context.ActivateShellAsync(context.CancellationToken).ConfigureAwait(false);

Task task = await this.Dispatcher.InvokeAsync(async () => { var viewModel = this.MainWindow.Content as MainViewModel; if (viewModel is null) { return; } viewModel.TrySelectCard(cardID); }).Task.ConfigureAwait(false);

await task.ConfigureAwait(false); context.Handled = true; }

#endregion } }

Note

Как вы можете видеть из App.xaml.cs в качестве главного окна приложения использован класс TessaWindow (Tessa.UI.Windows.TessaWindow), поэтому файлы MainWindow.xaml и MainWindow.xaml.cs (файл поддержки), генерируемые шаблоном проекта WPF Application, нам не нужны. Удалите их из проекта.

Задание собственного заголовка приложения через конфигурационный файл

Обратите внимание, что в заголовке приложения выводится информация из атрибутов Title и TitleDescription конфигурационного файла app.json. В случае отсутствия текста в Title в качестве имени будет использоваться название приложения, в нашем случае TessaDesktopApp.

Создание расширения проверки вхождения текущего пользователя в роль “Регистраторы”

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

В проекте Tessa.Extensions.Server создайте папку Initialization, в которой создайте новый файл AbUserAccessCheckingInitializationExtension.cs со следующим содержимым:

using System; using System.Linq; using System.Threading.Tasks; using Tessa.Platform.Initialization; using Tessa.Platform.Validation; using Tessa.Roles;

namespace Tessa.Extensions.Server.Initialization { /// <summary> /// Серверное расширение, проверяющее при запуске приложения /// входит ли текущий пользователь в роль "Регистраторы". /// </summary> public sealed class AbUserAccessServerInitializationExtension : ServerInitializationExtension { #region Fields

/// <summary> /// Репозиторий ролей. /// </summary> private IRoleRepository roleRepository;

/// <summary> /// Идентификатор статической роли "Регистраторы". /// </summary> private static readonly Guid registratorsRoleID = new Guid("0071b103-0ffa-49da-8776-53b9c654d815");

#endregion

#region Constructors

/// <summary> /// Создаёт новый объект расширения проверки ролей. /// </summary> /// <param name="roleRepository">Ролевой репозиторий.</param> public AbUserAccessCheckingInitializationExtension(IRoleRepository roleRepository) { this.roleRepository = roleRepository; }

#endregion

#region Base overrides

/// <inheritdoc/> public override async Task AfterRequest(IServerInitializationExtensionContext context) { if (!context.RequestIsSuccessful) { return; }

// проверка var users = await roleRepository.GetUsersAsync(registratorsRoleID, context.CancellationToken); if (users.All(p => p.UserID != context.Session.User.ID)) { context.ValidationResult.AddError(this, "Доступ запрещён."); } }

#endregion } }

Далее, регистрируем наше расширение. Создайте в папке Initialization файл Registrator.cs со следующим содержимым:

using Tessa.Platform.Initialization; using Tessa.Platform.Runtime; using Tessa.Extensions.Shared; using Unity;

namespace Tessa.Extensions.Server.Initialization { [Registrator] public sealed class Registrator : RegistratorBase { public override void RegisterUnity() { this.UnityContainer.RegisterType<AbUserAccessServerInitializationExtension>(TypeLifetime.ContainerControlled); }

public override void RegisterExtensions(IExtensionContainer extensionContainer) { extensionContainer .RegisterExtension<IServerInitializationExtension, AbUserAccessServerInitializationExtension>(x => x .WithOrder(ExtensionStage.AfterPlatform) .WithUnity(this.UnityContainer) .WhenApplications(AbApplicationIdentifiers.TessaDesktopApp)); } } }

Регистрация платформенных и типовых расширений

Для реализации поддержки всех базовых возможностей платформы TESSA необходимо выполнить регистрацию набора перечисленных ниже стандартных расширений для нашего типа приложения. По умолчанию они зарегистрированы для приложения TessaClient.

Регистрация серверных расширений

В проекте Tessa.Extensions.Server создайте папку TessaDesktopApp, в которой создайте файл Registrator.cs со следующим содержимым:

using Tessa.Extensions.Default.Server.Initialization; using Tessa.Extensions.Default.Server.Workflow.KrProcess.Initialization; using Tessa.Extensions.Platform.Server.Initialization; using Tessa.Extensions.Shared; using Tessa.Platform.Initialization; using Tessa.Platform.Runtime;

namespace Tessa.Extensions.Server.TessaDesktopApp { [Registrator] public sealed class Registrator : RegistratorBase { public override void RegisterExtensions(IExtensionContainer extensionContainer) { extensionContainer .RegisterExtension<IServerInitializationExtension, KrServerInitializationExtension>(x => x .WhenApplications(AbApplicationIdentifiers.TessaDesktopApp)) .RegisterExtension<IServerInitializationExtension, GlobalButtonsInitializationExtension>(x => x .WhenApplications(AbApplicationIdentifiers.TessaDesktopApp)) .RegisterExtension<IServerInitializationExtension, CheckVersionServerInitializationExtension>(x => x .WhenApplications(AbApplicationIdentifiers.TessaDesktopApp)) .RegisterExtension<IServerInitializationExtension, LocalizationServerInitializationExtension>(x => x .WhenApplications(AbApplicationIdentifiers.TessaDesktopApp)) .RegisterExtension<IServerInitializationExtension, ViewServerInitializationExtension>(x => x .WhenApplications(AbApplicationIdentifiers.TessaDesktopApp)) .RegisterExtension<IServerInitializationExtension, SearchQueryServerInitializationExtension>(x => x .WhenApplications(AbApplicationIdentifiers.TessaDesktopApp)) .RegisterExtension<IServerInitializationExtension, WorkplaceServerInitializationExtension>(x => x .WhenApplications(AbApplicationIdentifiers.TessaDesktopApp)) .RegisterExtension<IServerInitializationExtension, CardServerInitializationExtension>(x => x .WhenApplications(AbApplicationIdentifiers.TessaDesktopApp)) .RegisterExtension<IServerInitializationExtension, LicenseServerInitializationExtension>(x => x .WhenApplications(AbApplicationIdentifiers.TessaDesktopApp)) .RegisterExtension<IServerInitializationExtension, SingletonServerInitializationExtension>(x => x .WhenApplications(AbApplicationIdentifiers.TessaDesktopApp)) .RegisterExtension<IServerInitializationExtension, FileTemplatesServerInitializationExtension>(x => x .WhenApplications(AbApplicationIdentifiers.TessaDesktopApp)) .RegisterExtension<IServerInitializationExtension, ForumServerInitializationExtension>(x => x .WhenApplications(AbApplicationIdentifiers.TessaDesktopApp)) .RegisterExtension<IServerInitializationExtension, PasswordExpiresSoonInitializationExtension>(x => x .WhenApplications(AbApplicationIdentifiers.TessaDesktopApp)) .RegisterExtension<IServerInitializationExtension, UserCipherInfoInitializationExtension>(x => x .WhenApplications(AbApplicationIdentifiers.TessaDesktopApp)) ; } } }

Регистрация клиентских расширений

В проекте Tessa.Extensions.Client создайте папку TessaDesktopApp, в которой создайте файл Registrator.cs со следующим содержимым:

using Tessa.Extensions.Default.Client.Initialization; using Tessa.Extensions.Platform.Client.Application; using Tessa.Extensions.Platform.Client.Initialization; using Tessa.Extensions.Platform.Shared.Initialization; using Tessa.Extensions.Shared; using Tessa.Platform.Initialization; using Tessa.Platform.Runtime;

namespace Tessa.Extensions.Client.TessaDesktopApp { [Registrator] public sealed class Registrator : RegistratorBase { public override void RegisterExtensions(IExtensionContainer extensionContainer) { extensionContainer .RegisterExtension<IClientInitializationExtension, KrClientInitializationExtension>(x => x .WhenApplications(AbApplicationIdentifiers.TessaDesktopApp)) .RegisterExtension<IClientInitializationExtension, CardClientInitializationExtension>(x => x .WhenApplications(AbApplicationIdentifiers.TessaDesktopApp)) .RegisterExtension<IClientInitializationExtension, LicenseClientInitializationExtension>(x => x .WhenApplications(AbApplicationIdentifiers.TessaDesktopApp)) .RegisterExtension<IClientInitializationExtension, SingletonClientInitializationExtension>(x => x .WhenApplications(AbApplicationIdentifiers.TessaDesktopApp)) .RegisterExtension<IApplicationExtension, ClientApplicationExtension>(x => x .WhenApplications(AbApplicationIdentifiers.TessaDesktopApp)) .RegisterExtension<IApplicationExtension, FileFinalizationApplicationExtension>(x => x .WhenApplications(AbApplicationIdentifiers.TessaDesktopApp)) .RegisterExtension<IClientInitializationExtension, LocalizationClientInitializationExtension>(x => x .WhenApplications(AbApplicationIdentifiers.TessaDesktopApp)) .RegisterExtension<IClientInitializationExtension, ViewClientInitializationExtension>(x => x .WhenApplications(AbApplicationIdentifiers.TessaDesktopApp)) .RegisterExtension<IClientInitializationExtension, SearchQueryClientInitializationExtension>(x => x .WhenApplications(AbApplicationIdentifiers.TessaDesktopApp)) .RegisterExtension<IClientInitializationExtension, WorkplaceClientInitializationExtension>(x => x .WhenApplications(AbApplicationIdentifiers.TessaDesktopApp)) .RegisterExtension<IClientInitializationExtension, FileTemplatesClientInitializationExtension>(x => x .WhenApplications(AbApplicationIdentifiers.TessaDesktopApp)) .RegisterExtension<IClientInitializationExtension, PasswordExpiresSoonNotificationInitializationExtension>(x => x .WhenApplications(AbApplicationIdentifiers.TessaDesktopApp)) .RegisterExtension<IClientInitializationExtension, ForumClientInitializationExtension>(x => x .WhenApplications(AbApplicationIdentifiers.TessaDesktopApp)) ; } } }

Примеры скриптов построения и публикации приложения

Один из возможных вариантов публикации приложения на сервер приложений заключается в использовании ключа publish при запуске приложения.

Но, далее мы рассмотрим публикацию приложения при помощи утилиты tadmin.

Перед публикацией необходимо собрать x86 и x64 версии приложения. Для этого можно использовать скрипт build.bat:

@echo off rem Use OEM-866 (Cyrillic) encoding

setlocal EnableExtensions setlocal EnableDelayedExpansion

set "AppName=TessaDesktopApp" set "ExtName=Tessa.Extensions.Client" set "Config=Release" set "CopyParams=/Q /R /Y"

set "CurrentDir=%~dp0" if "%CurrentDir:~-1%" == "\" ( set "CurrentDir=%CurrentDir:~0,-1%" ) set "SourcePath=%CurrentDir%" pushd "%CurrentDir%"

:Start cls

echo ^> Building %AppName% and extensions

call :BuildApp %AppName% "%SourcePath%" "%SourcePath%\app\%AppName%32" win-x86 call :BuildApp %AppName% "%SourcePath%" "%SourcePath%\app\%AppName%" win-x64

echo; echo ^> Done. Press any key to quit... pause>nul cls

:Finish popd endlocal goto :EOF

:BuildApp "name" "source" "target" "runtime"

rem name - имя exe-файла без расширения .exe rem source - папка проекта с приложением rem target - папка назначения rem runtime - идентификатор RID: win-x64 или win-x86

rd /s /q "%~3\">nul 2>&1

cd /D "%~2"

dotnet publish "%~1.csproj" -c %Config% -r %~4 -o "%~3\" for %%a in (cs, de, es, fr, it, ja, ko, pl, pt-BR, ru, tr, zh-Hans, zh-Hant) do rd /S /Q "%~3\%%a">nul 2>&1

dotnet publish "%SourcePath%\..\..\Extensions\%ExtName%\%ExtName%.csproj" -c %Config% -r "%~4" -o "%~3\%ExtName%"

for %%a in ( "Tessa.Extensions.Default.Shared.dll", "Tessa.Extensions.Shared.dll", "Tessa.Extensions.Default.Client.dll", "Tessa.Extensions.Client.dll" ) do ( echo F|xcopy "%~3\%ExtName%\%%~a" "%~3\extensions\%%~a" %CopyParams%>nul 2>&1 )

rd /s /q "%~3\%ExtName%"

goto :EOF

Этот скрипт готовит приложение для публикации. Обратите внимание на названия директорий с версиями приложения TessaDesktopApp (для x64) и TessaDesktopApp32 (для x86).

Далее мы используем две команды утилиты tadmin:

  • PackageApp - для создания карточки приложения;
  • ImportCards - для импорта созданных карточек.

Обратите внимание на использование параметра a:, задающего алиас приложения в команде PackageApp. Мы применим его для явной установки псевдонима приложения для обработки внешних ссылок, поскольку не во всех случаях он может быть получен из метаданных.

Далее представлены скрипты, выполняющие публикацию нашего приложения.

Скрипт публикации приложения для ОС Windows (publish.bat):

@echo off rem Use OEM-866 (Cyrillic) encoding

setlocal EnableExtensions setlocal EnableDelayedExpansion

set "Address=https://localhost/tessa"

set "Login=" set "Password=" set "Connection=default" set "CheckTimeout=20"

set "Tools=C:\tessa\tadmin"

set "AppName=TessaDesktopApp"

set "CurrentDir=%~dp0" if "%CurrentDir:~-1%" == "\" ( set "CurrentDir=%CurrentDir:~0,-1%" ) set "Applications=%CurrentDir%\app"

cd /d "%Tools%"

:Start cls echo This script will publish %AppName% echo; echo Please check connection string prior to publication in configuration file: echo %Tools%\app.json echo; echo [Address] = %Address% echo; echo Press any key to begin publication... pause>nul

cls echo Publishing %AppName% echo; echo [Address] = %Address%

echo ^> Checking connection to web service tadmin CheckService "/a:%Address%" "/u:%Login%" "/p:%Password%" /timeout:%CheckTimeout% /q if not "%ErrorLevel%"=="0" goto :Fail

echo ^> Publishing %AppName%

del /Q "%Applications%\*.jcard">nul 2>&1 tadmin PackageApp "%Applications%\%AppName%\%AppName%.dll" /64bit /api2 "/a:tessadesktopapp" "/out:%Applications%\%AppName%.jcard" /q if not "%ErrorLevel%"=="0" goto :Fail tadmin PackageApp "%Applications%\%AppName%32\%AppName%.dll" /api2 "/a:tessadesktopapp" "/out:%Applications%\%AppName%32.jcard" /q if not "%ErrorLevel%"=="0" goto :Fail tadmin ImportCards "%Applications%" "/a:%Address%" "/u:%Login%" "/p:%Password%" /q if not "%ErrorLevel%"=="0" goto :Fail del /Q "%Applications%\*.jcard">nul 2>&1

call :Cleanup

echo; echo %AppName% is published. echo Press any key to close... pause>nul cls goto :Finish

:Cleanup goto :EOF

:Fail echo; call :Cleanup echo Publication failed with error code: %ErrorLevel% echo See the details in log file: %Tools%\log.txt echo; echo Press any key to close... pause>nul cls goto :Finish

:Finish endlocal goto :EOF

Скрипт публикации приложения для ОС Linux (publish.sh):

#!/bin/bash Address="https://localhost"

Login="" Password="" Connection="default" CheckTimeout="20"

AppName=TessaDesktopApp

get_script_dir () { SOURCE="${BASH_SOURCE[0]}" # while $SOURCE is a symlink, resolve it while [ -h "$SOURCE" ]; do DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" SOURCE="$( readlink "$SOURCE" )" # if $SOURCE was a relative symlink (so no "/" as prefix, need to resolve it relative to the symlink base directory [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" done DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" echo "$DIR" } CurrentDir="$(get_script_dir)"

Applications="$CurrentDir/app"

# here must be path to tadmin executable Tools="$CurrentDir/tools" cd "$Tools"

cleanup () { : }

exit_on_error () { ErrorLevel="$?" if [ "$ErrorLevel" != "0" ]; then cleanup echo echo "Publication failed with error code: $ErrorLevel" echo "See the details in log file: $Tools/log.txt" exit 1 fi }

#Start echo "This script will publish $AppName" echo echo "Please check connection string prior to publication in configuration file:" echo "$Tools/app.json" echo echo "[Address] = $Address" echo read -n1 -r -p "Press any key to begin publication or Ctrl+C to exit..." key

echo echo "Publishing $AppName" echo

echo " > Checking connection to web service" ./tadmin CheckService "-a:$Address" "-u:$Login" "-p:$Password" "-timeout:$CheckTimeout" -q exit_on_error

echo " > Publishing $AppName" rm -f "$Applications/*.jcard" ./tadmin PackageApp "$Applications/$AppName/$AppName.dll" -64bit -api2 "-a:tessadesktopapp" "-out:$Applications/$AppName.jcard" -q exit_on_error ./tadmin PackageApp "$Applications/$AppName32/$AppName.dll" -api2 "-a:tessadesktopapp" "-out:$Applications/$AppName32.jcard" -q exit_on_error ./tadmin ImportCards "$Applications" "-a:$Address" "-u:$Login" "-p:$Password" -q exit_on_error rm -f "$Applications/*.jcard"

cleanup

echo echo "$AppName is published." exit 0

Итоговое приложение

Если вы выполнили все описанные действия, то вы увидите примерно (с точностью до наполнения карточками) следующее:

Файл проекта .csproj должен выглядеть следующим образом:

<Project Sdk="Microsoft.NET.Sdk">

<Import Project="$(ProjectDir)../../Tessa.targets" /> <Import Project="$(ProjectDir)../../Tessa.Extensions.targets" /> <Import Project="$(ProjectDir)../../Tessa.Runtime.targets" />

<PropertyGroup> <OutputType>WinExe</OutputType> <TargetFramework>net5.0-windows</TargetFramework> <UseWPF>true</UseWPF> <LangVersion>latest</LangVersion> <RestoreSources>$(RestoreSources);../../Bin/packages;https://api.nuget.org/v3/index.json</RestoreSources> <ApplicationIcon>TessaDesktopApp.ico</ApplicationIcon> </PropertyGroup>

<ItemGroup> <Compile Include="..\..\Extensions\Tessa.Extensions.Shared\AbApplicationIdentifiers.cs" Link="AbApplicationIdentifiers.cs" /> </ItemGroup>

<ItemGroup> <Content Include="TessaDesktopApp.ico" CopyToOutputDirectory="PreserveNewest" /> </ItemGroup>

<ItemGroup> <PackageReference Include="Tessa.UI" Version="3.6.0.5" /> </ItemGroup>

<ItemGroup> <None Update="app.json" CopyToOutputDirectory="PreserveNewest" /> <None Include="..\..\app-local-client.json" Link="app-local-client.json" CopyToOutputDirectory="PreserveNewest" /> <None Update="extensions\extensions.xml" CopyToOutputDirectory="PreserveNewest" /> <None Update="Syncfusion.SfChart.WPF.dll" CopyToOutputDirectory="PreserveNewest" /> </ItemGroup>

</Project>

Разработанное в этой статье приложение вы можете загрузить по этой ссылке.

Back to top