# Урок 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="/files/fSpwWzEJLo9jtoXuc4Mh" alt=""><figcaption></figcaption></figure>

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

<figure><img src="/files/3ccbAEBiVqlnuMbyUdD9" 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="/files/YVEUogzN5IRs7pt1Jvhw" alt=""><figcaption></figcaption></figure>

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

![](/files/DBXb2Hit06cnFQG62P7I)

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

<figure><img src="/files/aYlp6XQBHlHR9NHf3QoO" 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="/files/L1J2kbvEd7VzJSGLyMjx" alt=""><figcaption></figcaption></figure>

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

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

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

<figure><img src="/files/WUW6WNN56RAAs7y5H2AV" alt=""><figcaption></figcaption></figure>

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

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

<figure><img src="/files/Mh2TSwwR7LJKmxvIXJlH" alt=""><figcaption></figcaption></figure>

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

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

<figure><img src="/files/V4Zp6nZHVw02zWw1KgDT" alt=""><figcaption></figcaption></figure>

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

<figure><img src="/files/FwAHDBKaaWbut8XMmsPu" 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="/files/suiwmv5m2OgRrW9MCj65" alt=""><figcaption></figcaption></figure>

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

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

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

<figure><img src="/files/Tjhv7VYUCe0N29M09yRP" alt=""><figcaption></figcaption></figure>

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

<figure><img src="/files/QNS0J1d9Dp63Y7ERkyyg" alt=""><figcaption></figcaption></figure>

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

<figure><img src="/files/mko1mAeLVzULKU2JZstB" alt=""><figcaption></figcaption></figure>

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

<figure><img src="/files/FPBZaNXfDfMTs2mAF76O" alt=""><figcaption></figcaption></figure>

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

![](/files/pSEsbL872ih69kY9nG3I)

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

<figure><img src="/files/8AtxuNhQowysEyqh02Ky" 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="/files/ND6QkzpF8z5fO8QfXGkd" alt=""><figcaption></figcaption></figure>

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

<figure><img src="/files/AnyUS1HhxcPTg7zrXl1p" alt=""><figcaption></figcaption></figure>

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

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

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

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

<figure><img src="/files/cfLNXqICj6lwHvKcO5O6" 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="/files/MjzqXoCzRbcv94YMf06q" 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="/files/X39eDpd4JiLTl30lItvs" 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>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://wfsys.gitbook.io/wt-practice/customization/lesson_custom_command_on_form.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
