Расширение функциональности ролеориентированного клиента NAV 2009 с помощью подключаемых компонентов. Пример 3. Реализация интерфейса IValueControlAddInDefinition

Итак, мы сильно продвинулись в теории и практике подключаемых компонентов. По крайней мере, мы так думаем. К сожалению, от полного понимания мы по-прежнему очень далеки. В данной статье попробуем проникнуть в концепции чуть глубже.

Начнем издалека. На панели инструментов Toolbox Visual Studio был обнаружен элемент управления NumericUpDown. Тут же возникло желание опробовать его в деле и в проект NAV4U.StyleControl был добавлен второй класс.

Примечание. Можно было бы создать проект с нуля, но было лень заниматься добавлением ссылок на сборки. Подписывать библиотеку тоже не хотелось. О том как создавать библиотеку “с нуля” можно прочитать в первой статье.

Вот, собственно говоря, код класса:

namespace StyleControl     
 
{     
 
  [ControlAddInExport("NAV4U.NumericUpDown")]     
 
  public class ApNumericUpDown : StringControlAddInBase     
 
  {     
 
    protected override Control CreateControl()     
 
    {     
 
      NumericUpDown tbcontrol = new NumericUpDown();     
 
      return tbcontrol;     
 
    }     
 
  }     
 
}

 Данный компонент был зарегистрирован (добавлен в таблицу 2000000069 Client Add-in) и подключен к поле “Prepayment %” (свойство ControlAddIn) страницы 21 Customer Card.

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

Customer Card

Ничего в глаза не бросается?

Хотя страница находится в режиме просмотра (View Mode), в поле Prepayment % можно вводить значения (как непосредственно в поле, так и с помощью кнопок).

Это очевидно. Однако есть еще один момент, который лично я изначально пропустил. Поле Credit Limit (LCY), для которого также подключен компонент (правда другой) отображается правильно (его редактировать нельзя).

Почему так произошло?

Для начала вспомним иерархию классов и положение в ней элементов управления TextBox и NumericUpDown.

Control
__TextBoxBase
____TextBox
__ScrollableControl
____ContainerControl
______UpDownBase
________NumericUpDown

Хотя оба класса являются потомками класса Control, дальше их пути расходятся. При этом у классов появляются новые свойства и методы, переопределяются свойства и методы базового класса (тем более, что в классе Contol они абстрактные).

Теперь взглянем на иерархию классов из Microsoft.Dynamics.Framework.UI.Extensibility.dll

WinFormsControlAddInBase
__StringControlAddInBase

WinFormsControlAddInBase является базовым классом, StringControlAddInBase наследует ряд его методом и свойств. Основным отличием StringControlAddInBase от WinFormsControlAddInBase является то, что StringControlAddInBase реализует два интерфейса:

  • IValueControlAddInDefinition (обеспечивает доступ к свойству Value)
  • IEventControlAddInDefinition (обеспечивает возможность вызывать событие в ролеориентированном клиенте)

Изначально, я был уверен, что это и есть единственное отличие. Однако я ошибался – кроме реализации указанных интерфейсов класс StringControlAddInBase оптимизирован для использования с элементами управления типа TextBox. В частности, он умеет становиться нередактируемым, если страница находится в режиме просмотра.
Соответственно идеология подключаемых компонентов для NAV 2009 предполагает, что если хочется использовать другие элементы управления для них стоит создавать отдельные классы на базе WinFormsControlAddIn. Если необходимо обеспечить обмен информации с ролеориентированным клиентом, то также нужно реализовать указанные выше интерфейсы.

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

namespace StyleControl     
 
{     
 
  [ControlAddInExport("NAV4U.NumericUpDown")]     
 
  public class ApNumericUpDown: WinFormsControlAddInBase     
 
  {     
 
    protected override Control CreateControl()     
 
    {     
 
      NumericUpDown tbcontrol = new NumericUpDown();     
 
      return tbcontrol;     
 
    }    protected override void OnEditableChanged(bool editable)     
 
    {     
 
      if (this.Control.GetType() == typeof(NumericUpDown))     
 
      {     
 
        NumericUpDown mycontrol = (NumericUpDown)this.Control;     
 
        mycontrol.ReadOnly = !this.Site.Editable;     
 
        mycontrol.Increment = this.Site.Editable ? 1 : 0;     
 
      }     
 
      base.OnEditableChanged(editable);     
 
    }     
 
  }     
 
}

В первую очередь в качестве базового класса укажем WinFormsControlAddInBase. Метод CreateControl оставим без изменений. А вот метод OnEditableChanged следует переопределить.

Этот метод вызывается при изменении свойства Editable. Если открыть IL DASM, то можно увидеть, что в этом методе ничего не происходит. В том же IL DASM можно увидеть, что метод OnEditableChanged переопределен для класса StringControlAddInBase.

В любом случае код в методе OnEditableChanged требует пояснения. Начнем, как водится, издалека.

Базовый класс WinFormsControlAddInBase включает свойство Control типа Control. Это свойство имеет один аксессор – get. При первом обращении к данному свойству в аксессоре get происходит вызов метода CreateControl, который также возвращает значения типа Control. Обратите внимание – свойство Control типа Control, а внутри метода CreateControl мы создали экземпляр класса NumericUpDown, который является потомком класса Control.

Наследование

В классе NumericUpDown унаследованы методы, свойства и события базового класса (при этом они могли быть переопределены), кроме того в классе NumericUpDown реализован набор новых методов, свойств и события (методы, свойства и события класса называются членами класса).

В частности у класса Control есть только свойство Enabled, а вот класс NumericUpDown кроме Enabled обладает свойствами ReadOnly и Increment (и другими).

Проблема в том, что в методе OnEditableChanged нельзя использовать конструкцию this.Control.ReadOnly = false. У метода Control просто нет такого свойства.

Поэтому мы пошли на хитрость. В C# классы – являются ссылочными типами. Это значит, что сам экземпляр класса хранится в управляемой куче, а в переменной хранится только ссылка (указатель).

В нашем примере при вызове оператора new (выражение NumericUpDown tbcontrol = new NumericUpDown(); метод CreateControl). В управляемой куче выделяется память под экземпляр класса NumericUpDown со всеми его членами.
Когда метод CreateControl завершается переменная tbcontrol уничтожается, но перед этим ссылка на объект передается в свойство Control (через закрытое поле Control). Т.е. на тот же самый экземпляр класса NumericUpDown ссылает свойство типа Control. При этом все свойства, методы и события класса NumericUpDown остаются в памяти – в управляемой куче. Однако обратиться к ним нельзя – класс Control не предоставляет к ним доступа.

Собственно хитрость заключается в следующем. В методе OnEditableChanged была создана локальная переменная типа NumericUpDown. Далее этой переменной была передана ссылка на экземпляр класса NumericUpDownою, в который была скопирована ссылка из свойства Control.

NumericUpDown mycontrol = (NumericUpDown)this.Control;

 Managed Heep

На рисунке показано, что в памяти (управляемой куче) все время находится только один экземпляр класса NumericUpDown. В разные моменты времени на него ссылаются tbcontrol (типа NumericUpDown), Control (типа Control) и mycontrol (типа NumericUpDown). Объект Control может получить доступ только к своим (желтым) членам, объекты tbcontrol и mycontrol могут получить доступ как к членам базового класса (желтые), так и к своим (зеленые).

Проверка условия: if (this.Control.GetType() == typeof(NumericUpDown)) нужна для того, чтобы убедиться, что переменная Control на самом деле ссылается на экземпляр класса NumericUpDown. В принципе проверку можно опустить, но это не по Best Practice.

Итак, в данном методе мы устанавливаем свойства ReadOnly и Incremental в зависимости от значений свойств свойства Site. Да, свойство Site содержит ссылку на контейнер, в котором находится наш подключаемый компонент.
Вот как теперь выглядит подключаемый компонент. Обратите внимание, на различие в поведении при изменении режима страницы:

Customer Card (Edit mode, View mode)

Итак, поведение подключаемого компонента нас устраивает. За исключением одного нюанса – он не получает данные из ролеориентированного клиента, и назад данные не возвращает. Это нужно исправить, поэтому мы продолжим работу над компонентом.

Как уже неоднократно упоминалось – чтобы дополнительный компонент мог считывать/записывать данные в поле/переменную, указанную в SourceExpr нужно реализовать интерфейс IValueControlAddInDefinition<T>.
Интерфейс IValueControlAddInDefinition<T> использует обобщения, однако на настоящий момент ролеориентированный клиент поддерживает только две его реализации:

  • IValueControlAddInDefinition<String>
  • IValueControlAddInDefinition<Object>

Мы будем реализовывать интерфейс IValueControlAddInDefinition<String>. Для этого потребуется реализовать в классе два свойства:

  • Value типа <T> (в нашем случае это String)
  • ValueHasChanged типа bool

Ниже приведена реализация эти свойств в классе ApNumericUpDown:

  public class ApNumericUpDown : WinFormsControlAddInBase, IValueControlAddInDefinition<String>
  {
    private bool hasValueChanged;
    public ApNumericUpDown()
    {
      hasValueChanged = false;
    }

    public String Value
    {
      get
      {
        return Control.Text;
      }
      set
      {
        Control.Text = value;
        hasValueChanged = true;
      }
    }

    public bool HasValueChanged { get { return hasValueChanged; } }
    …
    }
  }
}

При определении класса мы указали, что в нем будет реализован интерфейс. Хотя в С# класс может наследовать от одного базового класса, интерфейсов в нем может быть реализовано несколько.

Также мы создали закрытое поле hasValueChanged, оно используется в аксессоре get свойства HasValueChanged. Хотя при объявлении переменной у нее значение false, для порядка, мы добавили для класса конструктор, в котором принудительно инициировали переменную.

Кроме того в классе определено свойство Value.

На самом деле не дополнительный компонент взаимодействует с ролеориентированным клиентом, а ролеориентированный клиент. В частности при инициализации компонента, ролеориентированный клиент вызывает свойство Value и передает в него значение.

Кроме того ролеориентированный клиент проверяет значение свойства ValueHasChanged и если оно возвращает значение true, то он считывает значение из свойства Value.

Примечание. Свойство ValueHasChanged всегда будет возвращать true, так как предварительно вызывается аксессор set свойства Value. Поэтому можно ограничиться и такой конструкцией: public bool HasValueChanged { get { return true; } }

Вот теперь все работает как надо.

Обратите внимание, что если реализовать методы Value и ValueHasChanged не указав в определении класса интерфейс IValueControlAddInDefinition<String>, то взаимодействия между ролеориентированным клиентом и подключаемым компонентом не будет. Судя по всему, ролеориентированный клиент выполняет примерно следующую проверку:

if (null != AddIn as IValueControlAddInDefinition<String>)
{
//запись и/или чтение в/из свойства AddIn.Value
}

Благодарности.

Автор выражает признательность своему коллеге, Проценко Сергею, за помощь в написании статьи.

Метки:



Оставьте свой отзыв!