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

Как говорится в статье Состав элементов архитектуры WT-программы серверная и клиентская части WT-приложения помимо штатных библиотек платформы (dll-файлов) могут включать кастомные библиотеки, созданные разработчиками для расширения функциональности платформы для приложения.

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

Если хотите начать практику с этого урока, то вам необходимо развернуть учебный проект по инструкции в статье Разворачивание проекта.

При разворачивании проекта используйте backup базы данных, который можете найти в архиве из раздела Ответы прошлого урока. Скопируйте папки Forms, Workflow и Patterns в папку с развернутым проектом, например, в папку D:\WT\Projects\Template\Projects\1. Template.

Инструкция по подключению шаблонов находится по ссылке.

Подготовка

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

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

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

Настройка Visual Studio

Запустите Visual Studio Installer.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Сборка проекта

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

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

В поле 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:

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

Отладка проекта

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

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

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

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

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

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

Кастомный объект

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

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

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

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

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

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

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

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

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

TemplateOrderEdit.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>

Подключение библиотеки

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

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

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

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

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

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

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

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

<UseWindowsForms>true</UseWindowsForms>
Полный синтаксис файла Template.csproj
Template.csproj
<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>

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

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

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

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

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

PaymentCounter.cs
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);
        }
    }
}

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

В классе 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 для импорта типов.

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

PaymentCounter.cs
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);
        }
    }
}

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

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

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

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

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

PaymentCounterSettings.cs
using System.Xml;

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

        }
    }
}

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

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

}

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

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

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

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

PaymentCounterSettings.cs
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";

Так как наш объект должен работать с данными из конкретных столбцов таблиц позиций заказа и оплат, то все вложенные тэги и их атрибуты являются обязательными для описания в xml-коде. Чтобы получить значение обязательного атрибута, воспользуемся статическим методом GetRequiredAttributeValue класса XmlParser:

PaymentCounterSettings.cs
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}\".");
}

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

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

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

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

PaymentCounterSettings.cs
public DatabaseTable PositionDatabaseTable { get; private set; }

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

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

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

PaymentCounterSettings.cs
public string ColumnSumToPayName { get; private set; }

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

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

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

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

PaymentCounterSettings.cs
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}\".");
}

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

Полный код файла PaymentCounterSettings.cs
PaymentCounterSettings.cs
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}\".");
            }
        }
    }
}

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

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

PaymentCounter.cs
private DatabaseTable _positionDatabaseTable;
private DatabaseTable _paymentDatabaseTable;

private string _columnSumToPayName;
private string _columnPaidSumName;

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

PaymentCounter.cs
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;
}

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

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

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

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

PaymentCounter.cs
_positionDatabaseTable.Change += RecountChangeHandler;
_paymentDatabaseTable.Change += RecountChangeHandler;

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

PaymentCounter.cs
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;
}

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

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

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

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

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

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

PaymentCounter.cs
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);
    }
}

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

Get-проперти

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

PaymentCounter.cs
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);
    }
}

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

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

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

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

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

Set-проперти

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

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

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

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

PaymentCounter.cs
private const string RECOUNT_PROPERTY = "Recount";

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

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

Теперь для этой команды создадим Execution на системное условие FormLoadedCondition, которое необходимо дополнительно прописать в тэге <Conditions>.

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

Полный код файла PaymentCounter.cs
PaymentCounter.cs
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
    }
}

Итоги

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

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

Ответы

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

Last updated