> For the complete documentation index, see [llms.txt](https://wfsys.gitbook.io/wt-practice/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://wfsys.gitbook.io/wt-practice/customization/lesson_custom_command_on_form.md).

# Урок 22. Создание кастомных команд для форм

Как говорится в статье [Состав элементов архитектуры WT-программы](https://wfsys.gitbook.io/workflow-technology/platform-description/project-architecture) серверная и клиентская части WT-приложения помимо штатных библиотек платформы (dll-файлов) могут включать кастомные библиотеки, созданные разработчиками для расширения функциональности платформы для приложения.

Этот урок посвящен созданию кастомной команды на клиентской части, а в следующем уроке мы узнаем, как создавать кастомные команды на сервере. Платформа Workflow Technology написана на языке C#, поэтому все кастомные команды и объекты будем так же писать на этом языке. В уроках в качестве среды разработки будем использовать Microsoft Visual Studio 2022.

{% hint style="success" %}
Если хотите начать практику с этого урока, то вам необходимо развернуть учебный проект по инструкции в статье [Разворачивание проекта](https://wfsys.gitbook.io/workflow-technology/setting-up-dev-environment/manual-deployment-project).

При разворачивании проекта используйте backup базы данных, который можете найти в архиве из раздела [Ответы](/wt-practice/multiplayer_mode/lesson_automatic-data-update.md#answer) прошлого урока. Скопируйте папки *Forms*, *Workflow* и *Patterns* в папку с развернутым проектом, например, в папку *D:\WT\Projects\Template\Projects\1. Template*.

Инструкция по подключению шаблонов находится по [ссылке](/wt-practice/main/lesson_list_form.md#podklyuchenie-shablonov-k-proektu).
{% endhint %}

## Подготовка <a href="#preparation" id="preparation"></a>

При разработке кастомных библиотек к проекту в Visual Studio будем подключать штатные библиотеки. В этом уроке нам понадобятся бинарники, которые мы скачивали при разворачивании [клиентской части](https://wfsys.gitbook.io/workflow-technology/setting-up-dev-environment/manual-deployment-project#client). Штатные библиотеки платформы WT позволят использовать классы из этих библиотек и создавать собственные классы, которые платформа WT будет с легкостью интегрировать в приложение.

В дальнейшей работе с платформой Workflow Technology при разработке новых приложений будет удобнее, если для штатных бинарников платформы будет отведен отдельный каталог, в котором они будут храниться. А при создании нового WT-приложения можно будет копировать бинарники и файлы конфигурации из соответствующих папок этого каталога в папку, где будет разворачиваться приложение. Такое разделение позволит хранить штатные библиотеки отдельно от кастомных, созданных для конкретного приложения, что упростит копирование dll-файлов платформы в новые приложения.

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

### Настройка Visual Studio <a href="#setting-up-visual-studio" id="setting-up-visual-studio"></a>

Запустите Visual Studio Installer.&#x20;

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FL0RSZGSUuafzlVNaQzEv%2Fimage.png?alt=media&amp;token=3d2cb98a-adde-4d4b-8a40-ce83273fb168" alt=""><figcaption></figcaption></figure>

По кнопке "Изменить" откроется окно установки компонент среды разработки:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FoQ5WbNwXYa1wpSWswu49%2Fimage.png?alt=media&amp;token=65b80093-ba27-4a6f-8c08-3d993437a409" alt=""><figcaption></figcaption></figure>

На вкладке "Рабочие нагрузки" поставьте галочку на компоненте **ASP.NET и разработка веб-приложений** и нажмите кнопку "Изменить".

### Создание проекта <a href="#create-new-project" id="create-new-project"></a>

В папке **\Template\Projects\1. Template** создадим папку **Objects**, в которой будут храниться исходники кастомных объектов для форм.

Запустим Visual Studio 2022.

На начальной странице выберем пункт **Create a new project** (Создать проект):

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FrjnvQQ0th7QnjJ5HmiGd%2Fimage.png?alt=media&amp;token=e2b53373-ab8f-44af-a3c2-e9314576fbac" alt=""><figcaption></figcaption></figure>

Если среда разработки Visual Studio уже открыта, то проект можно создать, выбрав пункт **File -> New -> Project...** в строке меню. А также нажав кнопку **New Project** на панели инструментов, или нажав комбинацию клавиш **Ctrl+Shift+N**.

![](https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FMaWyrnyWrAmOvM6HWzD4%2F00.png?alt=media\&token=10529397-2e32-40f1-97a1-22988c8733c0)

На странице **Create a new project** введите в поле поиска **library**.  Так как платформа написана на платформе **.NET Core**, то нам необходимо выбрать соответствующий тип приложения:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FtcEYVydMHx1tUU94MZ5a%2Fimage.png?alt=media&amp;token=46bc6009-7f56-4909-8e36-8e017eeb5684" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
Если вы не видите шаблоны .NET, вероятно, у вас не установлена требуемая рабочая нагрузка. В сообщении **Not finding what you're looking for?** (Не удается найти то, что ищете?) выберите ссылку **Install more tools and features** (Установка других средств и компонентов). Откроется  Visual Studio Installer. Убедитесь, что у вас установлена рабочая нагрузка **ASP.NET и разработка веб-приложений**.
{% endhint %}

В диалоговом окне **Configure your new project** (Настройка нового проекта) доступны параметры, позволяющие присвоить имя проекту (и решению), выбрать расположение на диск&#x435;**:**

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FAzkPjVZSqdcDerO04DZy%2Fimage.png?alt=media&amp;token=580ea2d8-50f2-479d-9cdd-fed5452556d2" alt=""><figcaption></figcaption></figure>

В поле **Project name** укажем имя нашего проекта - **Template**. В поле **Location** - ранее созданную папку Objects.

Галочку **Place solution and project in the same directory** можно снять.

В диалоговом окне **Additional information** (Дополнительные сведения) содержится параметр для выбора версии платформы:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FFOORwckj7RluB5XQAZEJ%2Fimage.png?alt=media&amp;token=6d4103ae-2b34-4f12-8f49-19550eab173a" alt=""><figcaption></figcaption></figure>

Оставим здесь **.NET Core 3.1 (Out of support)** и нажмем на кнопку Create.

По умолчанию новый проект имеет один пустой класс Class1 в файле Class1.cs:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FkcKrbXAudQk6aIEWjX7i%2Fimage.png?alt=media&amp;token=49a9e7cc-2eb4-4a5a-8e94-374b2ec71786" alt=""><figcaption></figcaption></figure>

### Сборка проекта <a href="#building-project" id="building-project"></a>

Откроем свойства проекта, вызвав контекстное меню и выбрав пункт **Properties**. Или нажав комбинацию клавиш **Alt+Enter**.

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FgMAvQPZgVueoh7hxuddj%2Fimage.png?alt=media&amp;token=11e579ec-7ef3-4e4d-8dd8-ddfaf1f2e8af" alt=""><figcaption></figcaption></figure>

На вкладке **Build->Events** необходимо прописать команду, которая будет выполняться после сборки решения:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FceUqpPWabdPs8GxXaWym%2Fimage.png?alt=media&amp;token=b65b4777-4e3d-4601-877c-ba9078dde31d" alt=""><figcaption></figcaption></figure>

В поле **Post-build event** пропишем команду копирования собранного dll-файл из папки проекта в папку с  установленной клиентской частью:

```
copy "$(ProjectDir)$(OutDir)$(TargetName).dll" "D:\WorkflowForms\Template"
```

По умолчанию Visual Studio собирает проект в папку:

\Template\Projects\1. Template\Objects\Template\Template\bin\Debug\netcoreapp3.1

Давайте пересоберем проект, для этого в контекстном меню выберем пункт **Build** или **Rebuild**:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FtzPtmNWGPXfxL6ZB1bHb%2Fimage.png?alt=media&amp;token=2497dbad-14f5-4711-9240-bee6e9df0769" alt=""><figcaption></figcaption></figure>

Проверьте, что файл Template.dll скопировался в папку с установленной клиентской частью.

### Отладка проекта <a href="#debug" id="debug"></a>

Чтобы запускать приложение напрямую из Visual Studio, сделаем настройки режима отладки. Для этого откроем свойства проекта и перейдем на вкладку **Debug**:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FX6sN32DodXxSL0MROgpK%2Fimage.png?alt=media&amp;token=1e4516ca-1269-4dc7-b5d6-879b94b0a834" alt=""><figcaption></figcaption></figure>

Кликнем по тексту **Open debug launch profiles UI**. В открывшемся окне профилей запуска кликнем по кнопке **Create a new profile**. В меню выберем пункт **Executable**:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FkxHlYFAnC1RN7XzrTS8S%2Fimage.png?alt=media&amp;token=a45e2edd-bde6-4746-8bc5-552beabf8df0" alt=""><figcaption></figcaption></figure>

Новый профиль сразу переименуем, кликнув по кнопке **Rename selected profile**:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2Fnf9jJAlTGxC6nG5INhfL%2Fimage.png?alt=media&amp;token=f3f7be1b-cdd3-4f33-ae0b-43ef7a71ff32" alt=""><figcaption></figcaption></figure>

В поле **Executable** укажем путь до exe-файла приложения, размещенного в папке развернутой клиентской части:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FHlfH90xQLpwSEAHrKRRg%2Fimage.png?alt=media&amp;token=494de3dc-c794-4e48-bcd5-097d3506fb5f" alt=""><figcaption></figcaption></figure>

Теперь мы можем запустить приложение, нажав клавишу **F5** или кнопку на панели инструментов:

![](https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2F889LgA6vPdrFp4hErn2U%2Fimage.png?alt=media\&token=feaf7483-fcf8-4bd2-b0be-7d11e8aca4cb)

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

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FawIbchJxJ67DDFmfywjM%2Fimage.png?alt=media&amp;token=2b925105-d630-43de-963a-70f19c5f1f50" alt=""><figcaption></figcaption></figure>

## Кастомный объект <a href="#custom-object" id="custom-object"></a>

В этом уроке мы создадим кастомный объект на форме заказа.&#x20;

Первым делом необходимо описать xml-код объекта, чтобы иметь представление, какие элементы нам понадобятся:

{% code title="TemplateOrderEdit.xml" %}

```xml
<MyObject Name="PaymentCounter" Type="PaymentCounter" Assembly="Template">
  <PositionDatabaseTable Name="OrderPositionDatabaseTable">
    <ColumnSumToPay Name="TotalPrice" />
  </PositionDatabaseTable>
  <PaymentDatabaseTable Name="OrderPaymentDatabaseTable">
    <ColumnPaidSum Name="Summ" />
  </PaymentDatabaseTable>
</MyObject>
```

{% endcode %}

Здесь стоит обратить внимание на несколько моментов.

Во-первых, у тэга `<MyObject>` есть атрибут `Assembly`. В нем указывается имя библиотеки, в которой описан класс объекта. Так как это кастомный объект, то в атрибуте указано имя кастомной сборки **Template**, которую мы создали ранее.

Во-вторых, в атрибуте `Type` указывается имя класса, который описывает логику работы объекта.

Итак, в объекте мы будем передавать имена таблиц и их колонок. По этим именам в C# коде мы сможем получать доступ к объектам формы и их свойствам.

Объект PaymentCounter должен будет рассчитывать сумму начислений (позиций заказа), сумму всех оплат и остаток к оплате. Эти три значения будем получать через get-проперти объекта и хранить в переменных на форме.

Ранее в уроках мы создавали переменные TotalSumToPayVariable, TotalPaidSumVariable и TotalDebtVariable, в которых обращались к таблицам и через get-проперти получали нужные значения. Перепишем эти переменные - теперь будем заполнять их через get-проперти кастомного объекта:

{% code title="TemplateOrderEdit.xml" %}

```xml
<MyObject Name="TotalSumToPayVariable" Type="Variable" Assembly="SimpleControls" ChangeForm="False">
  <Value>
    <Object Name="PaymentCounter">
      <Property Name="TotalSumToPay" />
    </Object>
  </Value>
</MyObject>

<MyObject Name="TotalPaidSumVariable" Type="Variable" Assembly="SimpleControls" ChangeForm="False">
  <Value>
    <Object Name="PaymentCounter">
      <Property Name="TotalPaidSum" />
    </Object>
  </Value>
</MyObject>

<MyObject Name="TotalDebtVariable" Type="Variable" Assembly="SimpleControls" ChangeForm="False">
  <Value>
    <Object Name="PaymentCounter">
      <Property Name="TotalDebt" />
    </Object>
  </Value>
</MyObject>
```

{% endcode %}

### Подключение библиотеки <a href="#connecting-library" id="connecting-library"></a>

Подключим библиотеки WT-платформы, необходимы для создания кастомных объектов форм.

Для этого в контекстном меню проекта выберем пункт **Add -> Project Reference...**:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FKVHC4OiONjPNG4qzufgZ%2Fimage.png?alt=media&amp;token=96028c91-2d0d-427c-b9de-ea6bdbe8bb07" alt=""><figcaption></figcaption></figure>

В открывшемся окне перейдем на вкладку **Browse**, где по кнопке **Browse...** откроем диалоговое окно выбора файлов и в папке со штатными бинарниками клиентской части найдем файлы WorkflowForms.dll и FormObjects.dll и добавим их.

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FjXQNjzEgCGuhykrTsTQr%2Fimage.png?alt=media&amp;token=63e3247f-27b4-4d29-aec0-5d5ead371ac6" alt=""><figcaption></figcaption></figure>

WorkflowForms.dll - основная библиотека форм.

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

Дальше необходимо подключить фреймворк **Microsoft.WindowsDesktop.App.WindowsForms**.

Для этого в контекстном меню выберем пункт **Edit Project File**:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FgZg26rTJPXwRQ7mhCF3k%2Fimage.png?alt=media&amp;token=8d3861e2-d04e-4aac-8fea-3b5cb714e5e1" alt=""><figcaption></figcaption></figure>

Откроется файл Template.csproj, в нем в тэг `<PropertyGroup>` добавим строчку:

```
<UseWindowsForms>true</UseWindowsForms>
```

<details>

<summary>Полный синтаксис файла Template.csproj</summary>

{% code title="Template.csproj" %}

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

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <UseWindowsForms>true</UseWindowsForms>
  </PropertyGroup>

  <Target Name="PostBuild" AfterTargets="PostBuildEvent">
    <Exec Command="copy &quot;$(ProjectDir)$(OutDir)$(TargetName).dll&quot; &quot;D:\WorkflowForms\_Template_new.v3&quot;" />
  </Target>

  <ItemGroup>
    <Reference Include="FormObjects">
      <HintPath>..\..\..\..\..\..\..\..\WT\WorkflowForms.v3 x64\bin\FormObjects.dll</HintPath>
    </Reference>
    <Reference Include="WorkflowForms">
      <HintPath>..\..\..\..\..\..\..\..\WT\WorkflowForms.v3 x64\bin\WorkflowForms.dll</HintPath>
    </Reference>
  </ItemGroup>

</Project>
```

{% endcode %}

</details>

Таким образом, структура решения будет иметь вид:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2Fl4TWAqLKn3b6Aod2FUaj%2Fimage.png?alt=media&amp;token=c328d19d-be04-403b-95fe-5de4cac75827" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
Через файл Template.csproj так же можно подключать необходимые библиотеки, добавляя в тэг `<ItemGroup>` новый тэг `<Reference>` вида:

```xml
<Reference Include="WorkflowForms">
  <HintPath>..\..\..\..\..\..\..\..\WT\WorkflowForms.v3 x64\bin\Common.dll</HintPath>
</Reference>
```

{% endhint %}

### Создание класса

Удалим файл Class1.cs и создадим папку CustomControls, в которую добавим новый класс PaymentCounter:

{% code title="PaymentCounter.cs" %}

```csharp
using System.Xml;

namespace WorkflowForms.Controls
{
    public class PaymentCounter : Control
    {
        public override object Value
        {
            get
            {
                return null;
            }
            set
            { }
        }

        public override bool Visible
        {
            get
            {
                return false;
            }
            set
            {
            }
        }

        public override bool Enabled
        {
            get
            {
                return false;
            }
            set
            {
            }
        }

        protected override void UpdateValue()
        {
        }

        public PaymentCounter(IWorkflowForm form, XmlNode node) : base(form, node)
        {
        }

        protected override void InternalInit(XmlNode node, IWorkflowForm form, IControl parentControl, System.Windows.Forms.Control parentWindowsControl)
        {
            base.InternalInit(node, form, parentControl, parentWindowsControl);
        }
    }
}
```

{% endcode %}

Это минимально необходимый код для создания кастомного объекта.

В классе PaymentCounter мы переопределили свойства: Value (значение объекта), Visible (признак видимости объекта) и Enabled (признак активности объекта).

Так как кастомный объект не будет иметь значения, то его свойство Value возвращает null. И раз объект не будет иметь графического представления на форме, то свойства Visible и Enabled возвращают false.

Метод UpdateValue переопределяется для реализации логики обновления значения свойства Value. Подробнее об этом в примерах кастомных элементов, описанных в приложении к блоку.

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

Метод InternalInit принимает параметры:

* Первый параметр XmlNode `node` - это сам тэг `<MyObject>` и его содержимое.
* Второй параметр IWorkflowForm `form` - наша форма. Через нее сможем получать доступ ко всем элементам формы.
* Третий параметр IControl `parentControl` - родительский объект формы, в котором описан текущий. В нашем случае это TotalPanel.
* Четвертый параметр System.Windows.Forms.Control `parentWindowsControl` - родительский контрол, который является экземпляром класса System.Windows.Forms.Panel.

Обратите внимание, что для кастомных контролов в качестве `namespace` указывается **WorkflowForms.Controls**. Это позволяет обращаться к публичным классам штатных библиотек платформы без использования директивы `using` для импорта типов.

Давайте немного изменим начальный код класса:

{% code title="PaymentCounter.cs" %}

```csharp
using System;
using System.Xml;

namespace WorkflowForms.Controls
{
    public class PaymentCounter : Control
    {
        #region Base Properties
        public override object Value { get => null; set => throw new NotImplementedException(); }

        public override bool Visible { get => false; set => throw new NotImplementedException(); }

        public override bool Enabled { get => false; set => throw new NotImplementedException(); }
        #endregion

        protected override void UpdateValue()
        {
            throw new NotImplementedException();
        }

        public PaymentCounter(IWorkflowForm form, XmlNode node) : base(form, node)
        {
        }

        protected override void InternalInit(XmlNode node, IWorkflowForm form, IControl parentControl, System.Windows.Forms.Control parentWindowsControl)
        {
            base.InternalInit(node, form, parentControl, parentWindowsControl);
        }
    }
}
```

{% endcode %}

В свойства Value, Visible и Enabled мы добавили генерацию ошибки NotImplementedException, чтобы обозначить, что для данного класса эти свойства не реализованы. Аналогичную ошибку генерируем в методе UpdateValue.

Также добавили директиву #region, чтобы объединить базовые свойства класса в единый логический блок кода, который можно сворачивать.

### Парсинг xml-кода

Получать данные из xml-файла можно в текущем классе (PaymentCounter). Либо можно создать дополнительный класс настроек, чтобы разделить парсинг xml и бизнес-логику.

Давайте создадим класс PaymentCounterSettings:

{% code title="PaymentCounterSettings.cs" %}

```csharp
using System.Xml;

namespace WorkflowForms.Controls
{
    public class PaymentCounterSettings : ControlSettings
    {
        public PaymentCounterSettings(XmlNode node, IWorkflowForm form) : base(node, form)
        {

        }
    }
}
```

{% endcode %}

Вернемся в файл PaymentCounter.cs и создадим метод, в котором будем получать данные от PaymentCounterSettings:

{% code title="PaymentCounter.cs" %}

```csharp
private void LoadSettings(XmlNode node, IWorkflowForm form)
{
    var settings = new PaymentCounterSettings(node, form);

}
```

{% endcode %}

Добавим вызов этого метода в InternalInit.

Вернемся в файл PaymentCounterSettings.cs и вспомним описание нашего объекта на xml:

{% code title="TemplateOrderEdit.xml" %}

```xml
<MyObject Name="PaymentCounter" Type="PaymentCounter" Assembly="Template">
  <PositionDatabaseTable Name="OrderPositionDatabaseTable">              
    <ColumnSumToPay Name="TotalPrice" />
  </PositionDatabaseTable>
  <PaymentDatabaseTable Name="OrderPaymentDatabaseTable">
    <ColumnPaidSum Name="Summ" />
  </PaymentDatabaseTable>
</MyObject>
```

{% endcode %}

Для начала создадим константы с именами вложенных тэгов:

{% code title="PaymentCounterSettings.cs" %}

```csharp
private const string POSITION_DATABASE_TABLE_ELEMENT = "PositionDatabaseTable";
private const string PAYMENT_DATABASE_TABLE_ELEMENT = "PaymentDatabaseTable";
private const string COLUMN_NAME_SUM_TO_PAY_ELEMENT = "ColumnSumToPay";
private const string COLUMN_NAME_PAID_SUM_ELEMENT = "ColumnPaidSum";
```

{% endcode %}

Так как наш объект должен работать с данными из конкретных столбцов таблиц позиций заказа и оплат, то все вложенные тэги и их атрибуты являются обязательными для описания в xml-коде. Чтобы получить значение обязательного атрибута, воспользуемся статическим методом [GetRequiredAttributeValue](https://wfsys.gitbook.io/wt-knowledge-base/customization-client/platform-classes/xmlparser/methods/get-required-attribute-value#get_required_attribute_value) класса [XmlParser](https://wfsys.gitbook.io/wt-knowledge-base/customization-client/platform-classes/xmlparser):

{% code title="PaymentCounterSettings.cs" %}

```csharp
if (node[POSITION_DATABASE_TABLE_ELEMENT] != null)
{
    var databaseTableName = XmlParser.GetRequiredAttributeValue<string>(
        form, node,
        POSITION_DATABASE_TABLE_ELEMENT,
        NAME_ATTRIBUTE);
}
else
{
    throw new InvalidXmlException($"Не задано имя таблицы в поле {POSITION_DATABASE_TABLE_ELEMENT} объекта \"{Name}\" типа \"{GetType().Name}\".");
}
```

{% endcode %}

Для того чтобы использовать исключение типа InvalidXmlException, подключите к проекту библиотеку Exceptions.dll, а в код добавьте необходимый using.

В метод GetRequiredAttributeValue первыми двумя параметрами передаем объект формы (IWorkflowForm form) и объект xml-узела (XmlNode node), который содержит xml-код нашего кастомного объекта. В третьем параметре (string path) указываем полный путь до тэга, значение атрибута которого нужно получить. Имя атрибута указывается в четвертом параметре (string attribute). В данном случае используется константа `NAME_ATTRIBUTE`, описанная в базовом классе. Если элемент или его атрибут отсутствует, будет возвращено исключение типа InvalidXmlException.

Подключите к проекту библиотеку ComplexControls.dll, чтобы иметь доступ до класса DatabaseTable.

Создадим проперти для хранения ссылок на таблицу PositionDatabaseTable:

{% code title="PaymentCounterSettings.cs" %}

```csharp
public DatabaseTable PositionDatabaseTable { get; private set; }
```

{% endcode %}

Зная имя таблицы, мы можем получить на нее ссылку. Для этого обратимся к объекту form и его методу GetControl. Добавим код в блок `if`:

{% code title="PaymentCounterSettings.cs" %}

```csharp
PositionDatabaseTable = (DatabaseTable)form.GetControl(databaseTableName);
```

{% endcode %}

Создадим проперти, в котором будем хранить имя колонки для расчета суммы, необходимой к оплате:

{% code title="PaymentCounterSettings.cs" %}

```csharp
public string ColumnSumToPayName { get; private set; }
```

{% endcode %}

С помощью метода GetRequiredAttributeValue так же получим значение атрибута `Name` тэга `<ColumnSumToPay>`:

{% code title="PaymentCounterSettings.cs" %}

```csharp
ColumnSumToPayName = XmlParser.GetRequiredAttributeValue<string>(
    form, node,
    $"{POSITION_DATABASE_TABLE_ELEMENT}/{COLUMN_NAME_SUM_TO_PAY_ELEMENT}",
    NAME_ATTRIBUTE);
```

{% endcode %}

Так как тэг `<ColumnSumToPay>` является вложенным, то полный путь до него включает имя внешнего тэга `<PositionDatabaseTable>` и его собственное имя, разделенные символом `/`.

Полный синтаксис блока `if` для получения информации об элементах формы с позициями заказа:

{% code title="PaymentCounterSettings.cs" %}

```csharp
if (node[POSITION_DATABASE_TABLE_ELEMENT] != null)
{
    var databaseTableName = XmlParser.GetRequiredAttributeValue<string>(
        form, node,
        POSITION_DATABASE_TABLE_ELEMENT,
        NAME_ATTRIBUTE);

    PositionDatabaseTable = (DatabaseTable)form.GetControl(databaseTableName);

    ColumnSumToPayName = XmlParser.GetRequiredAttributeValue<string>(
        form, node,
        $"{POSITION_DATABASE_TABLE_ELEMENT}/{COLUMN_NAME_SUM_TO_PAY_ELEMENT}",
        NAME_ATTRIBUTE);
}
else
{
    throw new InvalidXmlException($"Не задано имя таблицы в поле {POSITION_DATABASE_TABLE_ELEMENT} объекта \"{Name}\" типа \"{GetType().Name}\".");
}
```

{% endcode %}

Аналогично сделайте и для таблицы оплат (PaymentDatabaseTable).

<details>

<summary>Полный код файла PaymentCounterSettings.cs</summary>

{% code title="PaymentCounterSettings.cs" %}

```csharp
using System.Xml;
using WorkflowForms.Exceptions;

namespace WorkflowForms.Controls
{
    public class PaymentCounterSettings : ControlSettings
    {
        private const string POSITION_DATABASE_TABLE_ELEMENT = "PositionDatabaseTable";
        private const string PAYMENT_DATABASE_TABLE_ELEMENT = "PaymentDatabaseTable";
        private const string COLUMN_NAME_SUM_TO_PAY_ELEMENT = "ColumnSumToPay";
        private const string COLUMN_NAME_PAID_SUM_ELEMENT = "ColumnPaidSum";

        public DatabaseTable PositionDatabaseTable { get; private set; }

        public DatabaseTable PaymentDatabaseTable { get; private set; }

        public string ColumnSumToPayName { get; private set; }

        public string ColumnPaidSumName { get; private set; }

        public PaymentCounterSettings(XmlNode node, IWorkflowForm form) : base(node, form)
        {
            if (node[POSITION_DATABASE_TABLE_ELEMENT] != null)
            {
                var databaseTableName = XmlParser.GetRequiredAttributeValue<string>(
                    form, node,
                    POSITION_DATABASE_TABLE_ELEMENT,
                    NAME_ATTRIBUTE);

                PositionDatabaseTable = (DatabaseTable)form.GetControl(databaseTableName);

                ColumnSumToPayName = XmlParser.GetRequiredAttributeValue<string>(
                    form, node,
                    $"{POSITION_DATABASE_TABLE_ELEMENT}/{COLUMN_NAME_SUM_TO_PAY_ELEMENT}",
                    NAME_ATTRIBUTE);
            }
            else
            {
                throw new InvalidXmlException($"Не задано имя таблицы в поле {POSITION_DATABASE_TABLE_ELEMENT} объекта \"{Name}\" типа \"{GetType().Name}\".");
            }

            if (node[PAYMENT_DATABASE_TABLE_ELEMENT] != null)
            {
                var databaseTableName = XmlParser.GetRequiredAttributeValue<string>(
                    form, node,
                    PAYMENT_DATABASE_TABLE_ELEMENT,
                    NAME_ATTRIBUTE);

                PaymentDatabaseTable = (DatabaseTable)form.GetControl(databaseTableName);

                ColumnPaidSumName = XmlParser.GetRequiredAttributeValue<string>(
                    form, node,
                    $"{PAYMENT_DATABASE_TABLE_ELEMENT}/{COLUMN_NAME_PAID_SUM_ELEMENT}",
                    NAME_ATTRIBUTE);
            }
            else
            {
                throw new InvalidXmlException($"Не задано имя таблицы в поле {PAYMENT_DATABASE_TABLE_ELEMENT} объекта \"{Name}\" типа \"{GetType().Name}\".");
            }
        }
    }
}
```

{% endcode %}

</details>

Отлично! Теперь можем вернуться в файл PaymentCounter.cs и продолжить работу с методом LoadSettings.

Создадим переменные:

{% code title="PaymentCounter.cs" %}

```csharp
private DatabaseTable _positionDatabaseTable;
private DatabaseTable _paymentDatabaseTable;

private string _columnSumToPayName;
private string _columnPaidSumName;
```

{% endcode %}

Дополним метод LoadSettings:

{% code title="PaymentCounter.cs" %}

```csharp
private void LoadSettings(XmlNode node, IWorkflowForm form)
{
    var settings = new PaymentCounterSettings(node, form);
    
    _positionDatabaseTable = settings.PositionDatabaseTable;
    _columnSumToPayName = settings.ColumnSumToPayName;
    
    _paymentDatabaseTable = settings.PaymentDatabaseTable;
    _columnPaidSumName = settings.ColumnPaidSumName;
}
```

{% endcode %}

Отлично! У нас есть данные с формы, и мы можем перейти к реализации расчетов.

### Реализация бизнес-логики

Чтобы наш кастомный объект PaymentCounter самостоятельно следил за изменением данных в таблицах OrderPositionDatabaseTable и OrderPaymentDatabaseTable, создадим handler, который будет срабатывать каждый раз при изменении данных в таблицах.&#x20;

Добавим следующий код в метод InternalInit:

{% code title="PaymentCounter.cs" %}

```csharp
_positionDatabaseTable.Change += RecountChangeHandler;
_paymentDatabaseTable.Change += RecountChangeHandler;
```

{% endcode %}

И добавим код самого handler и метода, который он будет вызывать:

{% code title="PaymentCounter.cs" %}

```csharp
private void RecountChangeHandler(Object sender, EventArgs args)
{
    Recount();
}

private void Recount()
{
    TotalSumToPay = (double)_positionDatabaseTable.GetProperty("ColumnSum", new Dictionary<string, object>() { { "ColumnName", _columnSumToPayName } });
    TotalPaidSum = (double)_paymentDatabaseTable.GetProperty("ColumnSum", new Dictionary<string, object>() { { "ColumnName", _columnPaidSumName } });
    TotalDebt = TotalSumToPay - TotalPaidSum;
}
```

{% endcode %}

В методе Recount получаем суммы в колонках таблиц на форме и рассчитываем остаток, необходимый к оплате. Ниже создадим, в котором опишем переменные TotalDebt, TotalSumToPay и TotalPaidSum.

Через метод GetProperty можем обращаться к get-проперти объекта, передавая в качестве аргументов имя get-проперти и словарь параметров:

```csharp
_positionDatabaseTable.GetProperty(
    "ColumnSum",
    new Dictionary<string, object>() { { "ColumnName", _columnSumToPayName } }
)
```

Эта запись аналогична записи в xml-коде:

```xml
<Object Name="OrderPositionDatabaseTable">
  <Property Name="ColumnSum">
    <Parameters>
      <Parameter Name="ColumnName">TotalPrice</Parameter>
    </Parameters>
  </Property>
</Object>
```

Полученные значения сохраняются в свойства класса. Добавим в PaymentCounter.cs следующий код:

{% code title="PaymentCounter.cs" %}

```csharp
private const string TOTAL_SUM_TO_PAY_PROPERTY = "TotalSumToPay";
private const string TOTAL_PAID_SUM_PROPERTY = "TotalPaidSum";
private const string TOTAL_DEBT_PROPERTY = "TotalDebt";

private double _totaSumToPay;
private double TotalSumToPay
{
    get
    {
        return _totaSumToPay;
    }
    set
    {
        _totaSumToPay = value;
        OnPropertyChange(TOTAL_SUM_TO_PAY_PROPERTY);
    }
}

private double _totalPaidSum;
private double TotalPaidSum
{
    get
    {
        return _totalPaidSum;
    }
    set
    {
        _totalPaidSum = value;
        OnPropertyChange(TOTAL_PAID_SUM_PROPERTY);
    }
}

private double _totalDebt;
private double TotalDebt
{
    get
    {
        return _totalDebt;
    }
    set
    {
        _totalDebt = value;
        OnPropertyChange(TOTAL_DEBT_PROPERTY);
    }
}
```

{% endcode %}

Метод OnPropertyChange описан в родительском классе и необходим, чтобы объект формы рассылал сообщение о том, что значение его свойства изменилось. Благодаря этому переменные TotalSumToPayVariable, TotalPaidSumVariable и TotalDebtVariable будут получать свежие значения get-проперти.

### Get-проперти

Переопределим метод GetProperty, чтобы кастомный объект имел get-проперти, которые можно использовать на форме:

{% code title="PaymentCounter.cs" %}

```csharp
public override object GetProperty(string propertyName, Dictionary<string, object> parameters)
{
    if (propertyName.Equals(TOTAL_SUM_TO_PAY_PROPERTY, StringComparison.OrdinalIgnoreCase))
    {
        return TotalSumToPay;
    }
    else if (propertyName.Equals(TOTAL_PAID_SUM_PROPERTY, StringComparison.OrdinalIgnoreCase))
    {
        return TotalPaidSum;
    }
    else if (propertyName.Equals(TOTAL_DEBT_PROPERTY, StringComparison.OrdinalIgnoreCase))
    {
        return TotalDebt;
    }
    else
    {
        return base.GetProperty(propertyName, parameters);
    }
}
```

{% endcode %}

Этот метод форма будет вызывать каждый раз, когда происходит обращение к get-проперти объекта. Таким образом, когда метод OnPropertyChange делает рассылку сообщения об изменении свойства объекта, переменные на форме, поймав это сообщение, инициируют вызов этого метода.

Первым делом проверяем, было ли обращение к нашему get-проперти и возвращаем соответствующее значение, иначе передаем вызов в базовый метод.

Запустим проект из Visual Studio, чтобы проверить значения в полях TotalSumToPayTextBox, TotalPaidSumTextBox и TotalDebtTextBox:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2Fmts2MvLNgYZtrOqMNSra%2Fimage.png?alt=media&amp;token=36db42fc-e52e-4957-aa59-2580ebd4a1e7" alt=""><figcaption></figcaption></figure>

Но если мы внесем изменения в одну из таблиц, то значения текстовых полей обновятся. Это связано с тем, что метод Recount вызывается только в момент изменения таблиц OrderPositionDatabaseTable и OrderPaymentDatabaseTable, а не при открытии формы.&#x20;

Реализуем set-проперти Recount, которое будет инициировать вызов метода Recount.

### Set-проперти

Чтобы кастомный объект имел set-проперти, необходимо переопределить метод SetProperty:

{% code title="PaymentCounter.cs" %}

```csharp
public override void SetProperty(string propertyName, Dictionary<string, object> parameters)
{
    if (propertyName.Equals(RECOUNT_PROPERTY, StringComparison.OrdinalIgnoreCase))
    {
        Recount();
    }
    else
    {
        base.SetProperty(propertyName, parameters);
    }
}
```

{% endcode %}

Этот метод вызывается в момент выполнения команды ValueSetCommand.

Добавим в класс константу:

{% code title="PaymentCounter.cs" %}

```csharp
private const string RECOUNT_PROPERTY = "Recount";
```

{% endcode %}

Добавим на форму команду ValueSetCommand, через которую обратимся к set-проперти Recount:

{% code title="TemplateOrderEdit.xml" %}

```xml
<Command Name="PaymentCounterRecountValueSetCommand" Type="ValueSetCommand" Assembly="Commands">
  <Object Name="PaymentCounter">
    <Property Name="Recount" />
  </Object>
</Command>
```

{% endcode %}

Теперь для этой команды создадим Execution на системное условие [FormLoadedCondition](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/conditions/event_condition/form_loaded_condition), которое необходимо дополнительно прописать в тэге `<Conditions>`.

Запустите проект из Visual Studio и проверьте отображение данных на форме заказа.

<details>

<summary>Полный код файла PaymentCounter.cs</summary>

{% code title="PaymentCounter.cs" %}

```csharp
using System;
using System.Collections.Generic;
using System.Xml;

namespace WorkflowForms.Controls
{
    public class PaymentCounter : Control
    {
        private const string TOTAL_SUM_TO_PAY_PROPERTY = "TotalSumToPay";
        private const string TOTAL_PAID_SUM_PROPERTY = "TotalPaidSum";
        private const string TOTAL_DEBT_PROPERTY = "TotalDebt";
        private const string RECOUNT_PROPERTY = "Recount";

        #region Base Properties
        public override object Value { get => null; set => throw new NotImplementedException(); }

        public override bool Visible { get => false; set => throw new NotImplementedException(); }

        public override bool Enabled { get => false; set => throw new NotImplementedException(); }
        #endregion

        #region Custom Properties
        private double _totaSumToPay;
        private double TotalSumToPay
        {
            get
            {
                return _totaSumToPay;
            }
            set
            {
                _totaSumToPay = value;
                OnPropertyChange(TOTAL_SUM_TO_PAY_PROPERTY);
            }
        }

        private double _totalPaidSum;
        private double TotalPaidSum
        {
            get
            {
                return _totalPaidSum;
            }
            set
            {
                _totalPaidSum = value;
                OnPropertyChange(TOTAL_PAID_SUM_PROPERTY);
            }
        }

        private double _totalDebt;
        private double TotalDebt
        {
            get
            {
                return _totalDebt;
            }
            set
            {
                _totalDebt = value;
                OnPropertyChange(TOTAL_DEBT_PROPERTY);
            }
        }
        #endregion

        private DatabaseTable _positionDatabaseTable;
        private DatabaseTable _paymentDatabaseTable;

        private string _columnSumToPayName;
        private string _columnPaidSumName;

        protected override void UpdateValue()
        {
            throw new NotImplementedException();
        }

        public PaymentCounter(IWorkflowForm form, XmlNode node) : base(form, node)
        {
        }

        #region Init
        protected override void InternalInit(XmlNode node, IWorkflowForm form, IControl parentControl, System.Windows.Forms.Control parentWindowsControl)
        {
            base.InternalInit(node, form, parentControl, parentWindowsControl);

            LoadSettings(node, form);

            _positionDatabaseTable.Change += RecountChangeHandler;
            _paymentDatabaseTable.Change += RecountChangeHandler;
        }

        private void LoadSettings(XmlNode node, IWorkflowForm form)
        {
            var settings = new PaymentCounterSettings(node, form);

            _positionDatabaseTable = settings.PositionDatabaseTable;
            _columnSumToPayName = settings.ColumnSumToPayName;

            _paymentDatabaseTable = settings.PaymentDatabaseTable;
            _columnPaidSumName = settings.ColumnPaidSumName;
        }
        #endregion

        private void RecountChangeHandler(Object sender, EventArgs args)
        {
            Recount();
        }

        private void Recount()
        {
            TotalSumToPay = (double)_positionDatabaseTable.GetProperty("ColumnSum", new Dictionary<string, object>() { { "ColumnName", _columnSumToPayName } });
            TotalPaidSum = (double)_paymentDatabaseTable.GetProperty("ColumnSum", new Dictionary<string, object>() { { "ColumnName", _columnPaidSumName } });
            TotalDebt = TotalSumToPay - TotalPaidSum;
        }

        #region Get Property
        public override object GetProperty(string propertyName, Dictionary<string, object> parameters)
        {
            if (propertyName.Equals(TOTAL_SUM_TO_PAY_PROPERTY, StringComparison.OrdinalIgnoreCase))
            {
                return TotalSumToPay;
            }
            else if (propertyName.Equals(TOTAL_PAID_SUM_PROPERTY, StringComparison.OrdinalIgnoreCase))
            {
                return TotalPaidSum;
            }
            else if (propertyName.Equals(TOTAL_DEBT_PROPERTY, StringComparison.OrdinalIgnoreCase))
            {
                return TotalDebt;
            }
            else
            {
                return base.GetProperty(propertyName, parameters);
            }
        }
        #endregion

        #region Set Property
        public override void SetProperty(string propertyName, Dictionary<string, object> parameters)
        {
            if (propertyName.Equals(RECOUNT_PROPERTY, StringComparison.OrdinalIgnoreCase))
            {
                Recount();
            }
            else
            {
                base.SetProperty(propertyName, parameters);
            }
        }
        #endregion
    }
}
```

{% endcode %}

</details>

## Итоги <a href="#results" id="results"></a>

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

В приложении [Клиентская часть](https://wfsys.gitbook.io/wt_knowledge_base/customization/client) собраны статьи о написании кастомных условий, команд и DataConnection.

## Ответы <a href="#answer" id="answer"></a>

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

<table data-card-size="large" data-view="cards"><thead><tr><th></th><th data-hidden data-type="content-ref"></th></tr></thead><tbody><tr><td>lesson22-answer.zip</td><td><a href="https://wfsys.ru/download/wt_practice_desktop_answers/lesson22-answer.zip">https://wfsys.ru/download/wt_practice_desktop_answers/lesson22-answer.zip</a></td></tr></tbody></table>
