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

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

Начнем с того, что предлагается два способа обмена информацией между ролеориентированным клиентом:

  1. С помощью свойства Value, объявленного в интерфейсе IValueControlAddInDefinition
  2. С помощью метода RaiseControlAddInEvent, объявленного в интерфейсе IEventControlAddInDefinition

Оба эти интерфейса реализованы в классе StringControlAddInBase.

Рассмотрим оба свойства подробнее.

Свойство Value.

Свойство Value, реализованные в классе StringControlAddInBase связано со свойством SourceExpr элемента страницы. С помощью этого свойства класс может получать информацию из базы данных, а также обновлять эту информацию.

У свойства Value есть два аксессора: set и get. С помощью этих аксессоров выполняется чтение и запись значения свойства.

Т.е. при вызове конструкции: Value = “1234″; система вызовет аксессор set и выполнит содержащийся в нем код.
При вызове конструкции MessageBox.Show(Value); система вызовет акссессор get и выполнит содержащийся в нем код.

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

При инициализации подключаемого компонента, компонент не знает какое значение он должен отображать пользователю. Поэтому ролеориентированный клиент передает это значение (из свойства SourceExpr) в свойство Value. При этом используется аксессор set. После того как пользователь изменил значение в подключаемом компоненте, ролеориентированный клиент считывает значение свойства Value, при этом используется аксессор get.
Свойство Value можно переопределить, а значит можно переопределить и его аксессоры. Для этого нужно в классе, основанном на классе StringControlAddInBase добавить следующий код:

public override string Value       
 
{       
 
  get       
 
  {       
 
    return base.Value;       
 
  }       
 
  set       
 
  {       
 
    base.Value = value;       
 
  }       
 
}

Это стандартный код аксессоров.

Если конструкцию return base.Value; заменить на return “5″, то в ролеориентированный клиент всегда будет возвращаться значение 5, независимо от того, что пользователь в ввел в поле.

Если же конструкцию base.Value = value; заменить на base.Value = “10″, то пользователю всегда будет отображаться значение 10, независимо от того, что на самом деле находится в базе данных (в свойстве SourceExpr).

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

Немножко архитектуры.

Класс, производный от класса StringControlAddInBase имеет свойство Control (элемент управления с визуальным представлением, например кнопка, текстовая зона и прочее http://msdn.microsoft.com/en-us/library/system.windows.forms.control.aspx)

Как легко догадаться инициализация этого свойства выполняется с помощью метода CreateControl. Этот метод должен переопределяться, что позволяет создавать свой собственный элемент управления: protected override Control CreateControl().

Так вот, элемент управления, производный от класса Control имеет свойство Text. В это свойство можно передавать некий текст (например, текст, отображаемый в текстовой зоне). Так же значение этого свойства можно считывать.
Все это очень напоминает свойство Value. Однако непонятно, как свойство Value класса производного от StringControlAddInBase становится свойством Text класса производного от Control.

С помощью IL DASM была исследована библиотека Microsoft.Dynamics.Framework.UI.Extensibility.dll. В библиотеке были рассмотрены методы-аксессоры set и get свойства Value, это дало возможность предположить, что исходный код свойства Value выглядел примерно следующим образом (на самом деле несколько иначе, но об этом позже):

public virtual string Value       
 
{       
 
  get       
 
  {       
 
    return this.Control.Text;       
 
  }       
 
  set       
 
  {       
 
    this.Control.Text = value;       
 
  }       
 
}

Метод RaiseControlAddInEvent

При вызове этого метода из кода класса происходит отработка триггера OnControlAddIn (триггер элемента страницы). В данный триггер передаются два параметра: Index типа Integer иText типа Text1024 (если в SourceExpr указана переменная типа BigText, то тип параметра также будет BigText).

OnControlAddIn

Ниже приведен пример использования данного метода в классе:

  int index = 1;       
 
  string data = "Hello, world!"       
 
  this.RaiseControlAddInEvent(index, data);

В самом Dynamics NAV в триггере OnControlAddIn разработчик указывает как система должна эти параметры обработать. Предположу, что так как триггер всего один, а задач он может выполнять много, то параметр Index используется для идентификации задачи. Параметр Data может содержать в себе целую строку параметров, которую следует разбирать самостоятельно (примерно как здесь)
Т.е. код в триггере OnControlAddIn может выглядеть следующим образом:

OnControlAddIn Code

Понятно, что в строке Data может быть и один параметр, а может параметров и не быть. Также надеюсь понятно, что текстовый параметр можно преобразовать во что-нибудь более подходящее, если использовать функцию EVALUATE.

Кстати любопытная мысль – поместить в параметр Data строку в формате XML.

Практика

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

Продолжим работу над компонентом NAV4U.WebAddin – в первую очередь хочется добавить возможность обновлять страницу. Для реализации этой возможности познакомимся с реализацией событий в C#.
Событие это совсем не то, что триггер в NAV, хотя с первого взгляда очень похоже. Чтобы понять работу событий рассмотрим такой пример.

В NAV есть таблица Товары, которая имеет множество сопутствующих таблиц (единицы измерения, варианты, замены). При удалении товара система вызывает триггер onDelete, в котором содержится код, который удаляет связанные записи в сопутствующих таблицах. С одной стороны это удобно – весь код находится в одном месте. С другой стороны – если вы изобретете новый функционал, использующий таблицу Товары, то вам потребуется изменить и триггер onDelete.

События в C# представляют собой гибкий список методов, которые должны быть вызваны при наступлении какого-либо события. Главная особенность заключается в том, что эти методы не являются жестко заданными. Классы могут самостоятельно подписываться на события.Например, у нас есть класс Товары, в нем есть событие onDetele. Класс Единицы Измерения подписывается на это событие, указав метод Items_onDelete. Когда событие наступает, то у класса Единицы Измерения вызывается метод Items_onDelete. Соответственно разработав новый класс, связанный с классом Товары, его нужно подписать на событие onDelete.

Сейчас рассмотрим все это на примере.

Для начала познакомимся с событием DocumentCompleted класса WebBrowser. В конструкторе подпишемся на событие DocumentCompleted и укажем какой метод вызывать при наступлении события.

  public apWebBrowser()       
 
  {       
 
    this.DocumentCompleted += new WebBrowserDocumentCompletedEventHandler(apWebBrowser_DocumentCompleted);       
 
    this.Navigate(ap_uri);       
 
  }

Метод apWebBrowser_DocumentCompleted является пользовательским, соответственно его тоже нужно включить в класс apWebBrowser.

void apWebBrowser_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)       
 
  {       
 
    if (this.Document != null)       
 
    {       
 
      MessageBox.Show(this.Document.Title);       
 
    }       
 
  }

Событие DocumentCompleted вызывается после завершения загрузки документа. После того как документ у класса WebBrowser (а значит и у apWebBrowser) заполняется свойство Document. Так как Document в свою очередь является классом HtmlDocument, то у него тоже есть свойства. Title – это свойство HtmlDocument, оно содержит название web-страницы.

Приведенный выше код делает следующее:

  1. Загружает страницу (метод Navigate)
  2. После загрузки страницы вызывает метод apWebBrowser_DocumentCompleted (благодаря подписке на событие DocumentCompleted)
  3. Если в свойство Document загружен HtmlDocument, то выполняется условие.
  4. Отображается на экране значение свойства Title.

 MessageBox

Обратите внимание, что класс подписался на собственное событие DocumentCompleted.

Продолжим. На самом деле нам абсолютно не важно значение свойства Title – этот MessageBox был больше для демонстрации. Дело в том, что у класса HtmlDocument кроме свойства Title, есть еще методы и события. Вот одно из событий нас и интересует. Это событие – ContextMenuShowing – отображение контекстного меню. Чтобы заменить стандартное контекстное меню на свое собственное нужно:

1. Изменить метод apWebBrowser_DocumentCompleted.

  void apWebBrowser_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)       
 
  {       
 
    if (this.Document != null)       
 
    {       
 
      this.Document.ContextMenuShowing += new HtmlElementEventHandler(Document_ContextMenuShowing);       
 
    }       
 
  }

Таким образом, мы подписались на событие ContextMenuShowing. При наступлении событии будет выполнен вызов метода Document_ContextMenuShowing.

2. Добавить метод Document_ContextMenuShowing

void Document_ContextMenuShowing(object sender, HtmlElementEventArgs e)       
 
  {       
 
    ContextMenuStrip m = new ContextMenuStrip();       
 
    m.Items.Add("Refresh", null, menuItem1_Click);       
 
    m.Show(this, e.MousePosition);    e.ReturnValue = false;       
 
  }

В этом методе мы сформировали новое контекстное меню из одного пункта – Refresh, а также указали, что при выборе этого пункта меню должен вызываться метод menuItem1_Click, код которого приведен ниже.

private void menuItem1_Click(object sender, EventArgs e)       
 
  {       
 
    this.Refresh();       
 
  }

Обратите внимание, что все три метода, которые вызываются при наступлении события, имеют два параметра: object типа sender и e. Параметр e – это класс EventArgs (либо производные от него классы) он используется для передачи параметров в функцию. Например класс HtmlElementEventArgs, который используется в событии ContextMenuShowing, содержит параметр MousePosition. Таким образом, можно отобразить контекстное меню в том месте, где пользователь щелкнул мышью (см. метод Show класса ContextMenuStrip).

На первый взгляд может показаться, что событие DocumentCompleted и метод apWebBrowser_DocumentCompleted абсолютно не нужны и можно подписаться на событие ContextMenuShowing непосредственно в конструкторе. Однако это не так. На самом деле непосредственно после выполнения метода Navigate в свойстве Document содержится значение null (загрузка выполняется в другом потоке), соответственно ни свойств, ни методов, ни событий у свойства Document тоже нет.

Nightmare

Нужно добавить еще пару пунктов меню для вызова стандартных страниц Microsoft Dynamics NAV 2009. Как мы уже знаем, для вызова триггера NAV нужно использовать метод RaiseControlAddInEvent. Однако этот метод определен в классе StringControlAddInBase , а мы создали класс WebAddin на базе класса WinFormsControlAddInBase. Но это полбеды. Хуже, что у класса WebAddin нет никакой связи с контекстным меню и его событиями. Дальше будет показано как обойти данные ограничения.

Начнем с простого – изменим базовый класс для класса WebAddin. Вместо WinFormsControlAddInBase укажем StringControlAddInBase. Если теперь осуществить сборку, то ролеориентированный клиент откажется работать без объяснения причин. После активации отладчика Visual Studio удалось получить внятное сообщение об ошибке: Компонент WebBrowser не поддерживает свойство Text.

Exception

Итак, проблема в свойстве Text. Самостоятельно мы туда ничего не пишем, и уж тем более не читаем. Но кто же это делает? А пишет в это свойство непосредственно ролеориентированный клиент с помощью свойства Value. Мы активировали свойство Value, изменив базовый класс на StringControlAddInBase, ради метода RaiseControlAddInEvent. Замкнутый круг?

Вовсе нет. Там же в начале статьи рассказывалось о возможности переопределения аксессоров Set и Get, этим мы и займемся:

В класс public class WebAddin : StringControlAddInBase добавьте определение свойства Value:

public override string Value { get; set; }

Как видно из кода, ни при вызове аксессора get, ни при вызове аксессора set ничего не происходит. После сборки наш подключаемый компонент вновь заработает, однако теперь он может вызывать триггер OnControlAddIn.
Теперь создадим пункты меню и соответствующие им методы.

В классе apWebBrowser в методе Document_ContextMenuShowing добавим два пункта меню: Items и Custormes.

  void Document_ContextMenuShowing(object sender, HtmlElementEventArgs e)       
 
  {       
 
    ContextMenuStrip m = new ContextMenuStrip();       
 
    m.Items.Add("Refresh", null, menuItem1_Click);       
 
    m.Items.Add("Items", null, menuItem2_Click);       
 
    m.Items.Add("Customer", null, menuItem3_Click);       
 
    m.Show(this, e.MousePosition);    e.ReturnValue = false;       
 
  }

Также в класс apWebBrowser добавим два пустых метода: menuItem2_Click и menuItem3_Click.

  private void menuItem2_Click(object sender, EventArgs e)       
 
  {       
 
  }       
 
private void menuItem3_Click(object sender, EventArgs e)       
 
  {       
 
  }

Теперь нужно каким-то образом уведомить класс WebAddin о том, что некое событие произошло и нужно вызывать метод RaiseControlAddInEvent.

Для этого мы используем технологию событий: мы создадим событие в классе apWebBrowser и подпишем на это событие класс WebAddin.

Для этого в классе apWebBrowser нужно определить член-событие:

  internal event EventHandler<EventArgs> apNewEvent;

apNewEvent – имя события, а тип члена-события - EventHandler<EventArgs>. EventHandler<EventArgs> - это делегат, согласно его определению метод обратного вызова должен выглядеть так: void MethodName(Object sender, EventArgs e).
Кроме этого нужно определить метод, ответственный за уведомление подписанных классов о наступлении события.

  internal virtual void OnapNewEvent(EventArgs e)
  {
    EventHandler<EventArgs> temp = apNewEvent;
    if (temp != null) temp(this, e);
  }

Этот метод проверяет: подписаны ли на событие другие классы, и если подписаны, то уведомляет подписанные классы о наступлении события.

Данный метод сам по себе не сработает – его нужно вызвать. Вставьте вызов данного метода в методы menuItem2_Click и menuItem3_Click:

  private void menuItem2_Click(object sender, EventArgs e)       
 
  {       
 
    OnapNewEvent(e);       
 
  }       
 
private void menuItem3_Click(object sender, EventArgs e)       
 
  {       
 
    OnapNewEvent(e);       
 
  }

Теперь класс WebAddin должен подписаться на данное событие, для этого измените метод CreateControl

protected override Control CreateControl()       
 
  {       
 
    var control = new apWebBrowser();       
 
    control.MaximumSize = new Size(400, 200);       
 
    control.ScrollBarsEnabled = false;       
 
    control.ScriptErrorsSuppressed = true;       
 
    control.WebBrowserShortcutsEnabled = false;       
 
control.apNewEvent += ap_Event;       
 
return control;       
 
  }

Обратите внимание, что раньше мы уже подписывались на событие, но синтаксис был сложнее. В данном случае нам на помощь пришел C#-компилятор, который неявно преобразовал выражение control.apNewEvent += ap_Event; в полную форму: control.apNewEvent = new EventHandler(ap_Event);

Также в класс WebAddin нужно добавить метод ap_Event, который будет отрабатывать при наступлении события:

  internal void ap_Event(object sender, EventArgs e)       
 
  {       
 
  this.RaiseControlAddInEvent(1,"nav4u.ru");       
 
  }

Теперь при выборе из контекстного меню пунктов Items или Customer будет срабатывать триггер aplink - OnControlAddIn(Index : Integer;Data : Text[1024]).

Но на самом деле этого мало: во-первых, в триггере нет никакого кода, а во-вторых, как система узнает какой именно пункт контекстного меню выбрал пользователь.

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

Итак, по умолчанию событие передает в качестве параметра класс EventArgs. Мы создадим на его основе новый класс, у которого будет несколько новых свойств.

Добавьте новый файл Event.cs в проект. В пространство имен NAV4U.WebAddin добавьте класс apNewEventArgs:

internal class apNewEventArgs : EventArgs 
{ 
  private readonly int ap_index; 
  private readonly String ap_data;       
 
  public apNewEventArgs(int index, String data) 
  { 
    ap_data = data; 
    ap_index = index; 
  }       
 
  public int Index { get { return ap_index; } } 
  public String Data { get { return ap_data; } } 
}

У данного класса два закрытых поля ap_index и ap_data. Эти поля заполняются при вызове конструктора с параметрами. Кроме того у класса определены два свойства Index и Data, они отвечают за считывание значений из полей класса.

Теперь используем этот класс в нашем событии. Измените в классе apWebBrowser член-событие:
internal event EventHandler<apNewEventArgs> apNewEvent;

Также измените метод, ответственный за уведомление подписанных классов о наступлении события.

  internal virtual void OnapNewEvent(apNewEventArgs e)
  {
    EventHandler<apNewEventArgs> temp = apNewEvent;
    if (temp != null) temp(this, e);
  }

И методы:

  private void menuItem2_Click(object sender, EventArgs e)       
 
  {       
 
    apNewEventArgs e2 = new apNewEventArgs(31, "SORTING(Search Description) WHERE(No.=FILTER(1000..1500))");       
 
    OnapNewEvent(e2);       
 
  }       
 
private void menuItem3_Click(object sender, EventArgs e)       
 
  {       
 
    apNewEventArgs e2 = new apNewEventArgs(22, "ORDER(Descending) WHERE(No.=FILTER(30000..40000))");       
 
    OnapNewEvent(e2);       
 
  }

В этих методах выполняется создание экземпляра класса apNewEventArgs с присвоением параметров.
Также нужно изменить метод ap_Event класса WebAddin

  internal void ap_Event(object sender, apNewEventArgs e)       
 
  {       
 
    this.RaiseControlAddInEvent(e.Index,e.Data);       
 
  }

Теперь метод RaiseControlAddInEvent передает в триггер onControlAddIn параметры Index и Data.
Добавим в триггер aplink - OnControlAddIn(Index : Integer;Data : Text[1024]) следующий код:

Code

Переменные r18 и r27 – переменные типа запись подтипа Customer и Item соотвественно.

Посмотрим, что получилось – запустим ролеориентированный клиент и щелкнем правой кнопкой мыши в веб-части NAV4U.WebAddin

Результат

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

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

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

Метки:



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