NAV4U (онлайн-журнал) » Blog Archive » Введение в веб-службы в Microsoft Dynamics NAV 2009. Часть 2.

Введение в веб-службы в Microsoft Dynamics NAV 2009. Часть 2.

В предыдущей статье мы познакомились с веб-службами Dynamics NAV 2009 и даже создали простое, очень простое приложение, которое считывает данные из Dynamics NAV. Хотя чтение данных не самая сложная задача мы все же детально разберемся с доступными методами.

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

dataGridView1.DataSource = ItemCardService.ReadMultiple(null, null, 0);

На код:

List<ItemCardWS.ItemCard_Filter filters = new List<ItemCardWS.ItemCard_Filter>();

ItemCardWS.ItemCard_Filter descFilter = new ItemCardWS.ItemCard_Filter(); 
descFilter.Field = ItemCardWS.ItemCard_Fields.Description;

descFilter.Criteria = “*a*&*p*”;

filters.Add(descFilter); 
dataGridView1.DataSource = ItemCardService.ReadMultiple(filters.ToArray(), null, 0);

Что делает приведенный выше код.

Вначале мы создаем список объектов типа фильтр по товару. Затем создаем объект типа фильтр по товару. Объект типа фильтр имеет свойства «Field», куда указывается поле, по которому происходит фильтрация и «Criteria» - собственно сама строка фильтра, которую обычно указывают в методе SETFILTER. В приведенном выше примере указано, что фильтровать надо по полю Описание (Description) и поле должно содержать инициалы автора на английском.

Затем объект добавляется в список объектов (кстати туда можно поместить несколько объектов с фильтрами по разным полям). Дальше вызывается метод ReadMultiple куда передается список объектов типа фильтр по товару (в виде массива).

Теперь при запуске наше приложение будет содержать значительно меньше записей.

Хорошо. С первым параметром разобрались. Теперь посмотрим на … третий. Этот параметр говорит о том, сколько записей нужно получить. Зачем он? Например, книгу товарных операций без фильтров можно довольно долго выкачивать через веб-службы. А этот параметр позволяет ограничить количество получаемых записей. В нашем примере этот параметр имеет значении 0, т.е. ограничений нет. Предлагаю установить значение 5 и запустить наше приложение.

dataGridView1.DataSource = ItemCardService.ReadMultiple(filters.ToArray(), null, 5);

Item List чтение пяти записей

Получилось неплохо. Всего пять строк. Приложение очень быстро получило данные от веб-службы и пользователь не успел испытать дискомфорт.

Однако как же быть, если этих данных маловато. Хочется знать какие же еще товары есть в справочнике. Как же «пролистать» дальше? Для этого мы воспользуемся вторым параметром метода ReadMultiple. Этот параметр принимает строку содержащую ключ и время получения записи. Данный параметр называется bookmarkKey и выглядит примерно так:

bookmarkKey

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

Им и воспользуемся.

Значит, чтобы получить следующий набор записей нужно в качестве параметра bookmarkKey передать свойство Key последней считанной записи (в нашем случае это строка с товаром 80100 Printing Paper).
Переход к следующему набору записей будет осуществляться по нажатию кнопки «Next». Добавим данную кнопку на форму. Напомню, что для элемента управления dataGridView1 я установил в свойстве Dock значение Fill, поэтому места для кнопки там уже не осталось. Чтобы освободить немного места в нижней части я заменил значение Fill на Top. Свойство Dock – это некий аналог свойств HorzGlue и VertGlue из Dynamics NAV. Значение Fill указывает, что элемент управления займет все свободное место на форме, а значение Top – говорит, что элемент будет прижиматься к верхней, левой и правой границе, при этом снизу место будет свободно.

На этом свободное место и нужно добавить элемент управления Button, изменить ему свойство Text (аналог Caption из Dynamics NAV) и дважды щелкнуть по элементу управления. При этом Visual Studio автоматически создаст метод и подпишет ее на событие Click.

Напомню, что я довольно подробно писал про архитектуру событий в C# в статьях посвященных подключаемым компонентам (Addin’ам), в частности здесь.

Добавим необходимый код в метод button1_Click.

DataGridViewRow row = dataGridView1.Rows[dataGridView1.Rows.Count - 1]; 
ItemCardWS.ItemCard item = row.DataBoundItem as ItemCardWS.ItemCard; 
if (item != null) 
{ 
  dataGridView1.DataSource = 
  ItemCardService.ReadMultiple(filters.ToArray(), item.Key, 5); 
}

Первая строка позволяет получить нам последнуюю строку из элемента управления dataGridView1. Для этого используется коллекция Rows и методы этой коллекции. Метод Count возвращает информацию о количестве строк, а с помощью квадратных скобок мы указываем номер нужного нам элемента. Обратите внимание, что элементы коллекции в C# нумеруются с нуля. Т.е. чтобы получить превый элемент нужно передать значение 0, а чтобы получить последний нужно передать количество элементов уменьшенное на 1. Т.е. если требуется получить 5-ый элемент, то нужно передать значение 4.

Во второй строке мы создаем объект типа ItemCardWS.ItemCard (запись) и присваиваем ему связанный элемент из строки. Не забываем про явное приведение типов и используем оператор as.

Кое что про приведение типов я рассказывал в статьях посвященных подключаемым компонентам (Addin’ам), в частности здесь.

Дальше проверяем, удалось ли нам получить связанную со строкой запись, и вызываем уже знакомый нам метод ReadMultiple, с заполненным вторым параметром.

Сразу предупрежу, что скомпилировать приложение не удастся, т.к. в методе button1_Click используется переменная filters объявленная в методе Form1_Load. Я немного исправил код и теперь он выглядит так:

Изменения кода

Красным выделены строки, связанные с изменением области видимости переменной filters.

На этом с ReadMultiple можно закончить и перейти к следующему методу. Предлагаю следующим назначить метод Read. Если ReadMultiple является аналогом FINDSET, то Read – это аналог метода GET.

Ему тоже передаются параметры в виде первичного ключа. Настоящего первичного ключа, а не bookmarkKey. Думаю можно использовать небольшой пример. Добавим в наше приложение еще одну кнопку и текстовую зону.

Для кнопки создадим метод Button2_Click и добавим туда следующий код:

if (textBox1.Text!=String.Empty) 
{ 
  ItemCardWS.ItemCard item = ItemCardService.Read(textBox1.Text); 
  if (item != null) 
  { 
  MessageBox.Show(item.Description); 
  } 
}

Вначале проверим, чтоб в текстовую зону введено значение (код товара). Далее считываем из веб службы требуемый товар (в качестве параметра передаем код товара). Если чтение удалось (т.е. объект item инициализирован), то отображаем сообщение с описание товара.

В целом должно получиться так:

Get

Как видим – ничего сложного в данном методе нет. Поэтому двигаемся дальше. К любимому делу – удалению. Для удаления используется метод Delete. Какая запись (одна запись) должна быть удалена определяется с помощью параметра bookmarkKey.

Добавим кнопку Delete. Для кнопки создадим метод Button2_Click и добавим туда следующий код:

String errors = ""; 
foreach (DataGridViewRow row in dataGridView1.SelectedRows) 
{ 
  ItemCardWS.ItemCard item = row.DataBoundItem as ItemCardWS.ItemCard; 
  if (item != null) 
  { 
    try 
    { 
      ItemCardService.Delete(item.Key); 
    } 
    catch (System.Web.Services.Protocols.SoapException ex) 
    { 
      if (errors==String.Empty) 
      { 
        errors = "При выполнении произошли следующие ошибки:"; 
      } 
      errors += '\n'+ ex.Message; 
    } 
  } 
} 
dataGridView1.DataSource = ItemCardService.ReadMultiple(filters.ToArray(), null, 5); 
if (errors!=String.Empty) 
  { 
    MessageBox.Show(errors); 
  } 
}

Пугаться не следует. На самом деле удаление не представляет сложности, а кода много лишь по той причине, что я решил слегка поразвлечься. В данном примере показано как удалять несколько записей, а также обработка ошибок, возникающих при работе с веб-службами.

Давайте разберемся, что же делает приведенный выше код. А делает он следующее. Для каждой выделенной записи в dataGridView1 мы получаем соответвующий объект типа ItemCardWS.ItemCard - за это отвечает цикл foreach. Далее пытаемся удалить объект. Метод Delete помещен между операторами try и catch, это позволяет обработать исключение. Если удалить запись не удалось, то в строку errors будет помещено описание ошибки.

Так как мы работаем с веб-службами, то и исключения у нас соотвествующие:

System.Web.Services.Protocols.SoapException

У исключения есть свойство Message его мы и сохраняем. Существуют три стандартных текста для исключений:

  • Other user has modified [record name]. – если другой пользователь изменил запись.
  • [record name] [field] [value] does not exist. – если запись не существует (кто-то удалил ее раньше)
  • Callback functions are not allowed. – если при удалении система чего-то спрашивает

Остальные генерируются из бизнес-логики Dynamics NAV и фактически являют собой текст, переданный в качестве параметра в функцию ERROR.

Но вернемся к коду. После того как все выделенные записи были обработаны мы вновь используем метод ReadMultiple и обновляем содержимое элемента управления dataGridView1. Если строка errors не пуста, то выводится сообщение с перечнем возникших ошибок. Ну чтож попробуем как это работает.
Запустим наше приложение, выделим несколько строк и нажмем кнопку Delete.

Перед удалением

Вот что мы увидем после выполнения нашего кода.

После удаления

Как видим два из трех товаров удалить не удалось. Один из них используется в заказе покупки, а другой включен в Спецификацию. А вот товара 80006 больше нет - он удален.

Примечание. На самом деле он также не хотел удаляться, т.к. использовался в модуле сервис, но после удаления всех ссылок (вручную), данный товар простился с нами.

Если операции чтения легко давались и ранее, например, с помощью прямого чтения из базы данных, то с удалением было все не так просто. Ведь при удалении обязательно должен отработать триггер onDelete иначе ссылочная целостность может быть нарушена. Как легко убедится с веб службами удаление записей из Dynamics NAV является довольно простой в реализации задачей.

Но пора бы перейти к созданию своих собственных записей. Для этого веб-служба предоставляет нам два метода. Начнем с более простого метода Create.

Наверное уже не следует говорить, что нужно создать кнопку и метод Button3_Click, куда нужно добавить следующий код:

ItemCardWS.ItemCard item = new ItemCardWS.ItemCard(); 
item.Description = "ap item1"; 
ItemCardService.Create(ref item);             
 
dataGridView1.DataSource = ItemCardService.ReadMultiple(filters.ToArray(), null, 5);

За создание записи отвечают первые три строки, где мы проинициализировали пустой объект типа ItemCardWS.ItemCard (аналог INIT), далее заполнили нужные поля (в данном случае ограничились лишь полем Description). После чего произвели вставку записи (аналог INSERT).

Последняя строка нужна лишь для отображения результата в элементе управления dataGridView1.

Вставка одной записи (Create)

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

ItemCardWS.ItemCard[] items = new ItemCardWS.ItemCard[3];

for (int i = 0; i < items.Length; i++)

{ 
  items[i] = new ItemCardWS.ItemCard(); 
  items[i].Description = "ap item mult"+i.ToString(); 
}             
 
ItemCardService.CreateMultiple(ref items); 
dataGridView1.DataSource = ItemCardService.ReadMultiple(filters.ToArray(), null, 10);

Все довольно прозрачно – объявляем массив объектов типа ItemCardWS.ItemCard. В цикле заполняем его проинициализированными объектами с нужными значениями. Далее выполняем вставку.

Вставка нескольких записей (CreateMultiple)

Получается примерно так, как показано на рисунке выше.

Обратите внимание, что как метод Create, так и CreateMultiple в качестве параметра принимают ссылку, а не само значение. А это значит, что после вставки, значение (или значения массива) будут дополнены новыми данными. В частности там окажется значение поля «No.», которое присвоилось в результате работы триггера onInsert.
Предлагаю удалить созданные записи (а для этого у нас есть специальная кнопка) и несколько расширить наши методы. Начнем с метода, вызываемого при нажатии кнопки Create1.

Теперь код в данном методе будет выглядеть так:

ItemCardWS.ItemCard item = new ItemCardWS.ItemCard(); 
item.Description = "ap item1"; 
ItemCardService.Create(ref item);             
 
MessageBox.Show("Выполнена вставка товара: "+item.Description 
  +"\nавтоматически заполнено поле No.: "+item.No);             
 
item.Item_Category_Code = "FURNITURE"; 
ItemCardService.Update(ref item);             
 
MessageBox.Show("Выполнено изменение товара "+item.No+",\n" 
  +"заполнено поле Item Category Code: " + item.Item_Category_Code 
  +"\nавтоматически заполнено поле Inventory Posting Group" + item.Inventory_Posting_Group);             
 
dataGridView1.DataSource = ItemCardService.ReadMultiple(filters.ToArray(), null, 5);

Начало кода прежнее. А вот после первого сообщения был использован метод Update (аналог MODIFY), в результате чего в товаре было заполнено поле “Item Category Code”, а также заполнен раб полей значениями по умолчанию из соответствующей товарной категории. В частности это поле “Inventory Posting Group”.

Вставка (Create) и изменение (Modify) одной записи

Повторим тоже самое для метода, вызываемого при нажатии кнопки Create Mult.

ItemCardWS.ItemCard[] items = new ItemCardWS.ItemCard[3];

for (int i = 0; i < items.Length; i++)

{ 
  items[i] = new ItemCardWS.ItemCard(); 
  items[i].Description = "ap item mult"+i.ToString(); 
} 
ItemCardService.CreateMultiple(ref items);

for (int i = 0; i < items.Length; i++)

{ 
  items[i].Description += items[i].No; 
  items[i].Item_Category_Code = "MISC"; 
} 
ItemCardService.UpdateMultiple(ref items);             
 
dataGridView1.DataSource = ItemCardService.ReadMultiple(filters.ToArray(), null, 10);

Начало метода оставим практически без изменений, а ниже добавим второй цикл по массиву записей. В цикле изменим значение поля Description, включив в него также значение поля No (чтобы убедиться, что после вставки поле No было заполнено). А также изменим поле “Item Category Code”.

Вставка (Create) и изменение (Modify) набора записей

Расширение функциональности предоставляемой веб службами.

Возможно кое-кто думает, что вся функциональность веб-служб ограничивается моделью CRUD и теми методами рассмотренными выше. Смею вас уверить это не так. Именно под конец статьи был припасен небольшой десерт.

Внимательный читатель, а я надеюсь, что меня читают именно они, уже обратил внимание, на поле Inventory, где отображается некая цифра. Откуда она берется я думаю пояснять не нужно. Напомню лишь, что она показывает текущее наличие товара по системе в целом, т.е. на всех складах. Однако наше приложение было бы намного полезнее если бы в данном поле отображалась информация о наличие товара на определенном складе. Например, на складе СИНИЙ (т.к. у меня версия международная, то склад называется BLUE).

Как же этого добиться?

Для начала хотел бы предостеречь от попыток использовать для этих целей полей Location Filter – ни к чему хорошему это не приведет. Дело в том, что коллекция ItemCardWS.ItemCard_Fields содержит только те поля, которые включены в страницу, а вовсе не все доступные поля таблицы. Например, здесь нет поля “Unit Price Including VAT”. Поля типа FlowFilter тоже отсутствуют. В карточке они выведены в отдельном разделе - Limit Totals.

Вычисляемые поля на странице типа карточка (FlowField и FlowFilter)

Как же быть?

Наверное пора использовать в качестве веб-службы программный модуль. Для этого его нужно написать. Думаю, что нет необходимости показывать как создается программный модуль с функцией, принимающей два параметра: код товара и код склада, и возвращающей значение типа Decimal. Про это можно прочесть во многих местах, в данном же случае хотелось бы поделиться одной хитростью.

А она заключается в том, что одна веб-службы может одновременно включать в себя как страницу так и программный модуль. Сейчас я покажу как это сделать.

Создадим программный модуль. Добавим в него функцию, первый параметр которой – запись типа Товар:

Публикуемый программный модуль (codeunit)

Обратите внимание – именно запись, а не код товара.

Далее сохраним программный модуль и опубликуем его.

Страница web services

Здесь аж два момента, на которые следует обратит внимание. Во-первых, это имя веб-службы, оно должно совпадать с именем веб-службы для страницы. В данном случае это ItemCard. Во-вторых, не нужно ставить флаг в поле Published.

Если бы мы добавляли в наш проект ссылку на веб-службу сейчас, то мы бы увидели, что в списке доступных методов модели CRUD, появился новый метод - WsCalcfield.

Add Web References

Но так как в нашем проекте уже есть ссылка на веб-службу нам нужно ее всего лишь обновить. Для этого в Solution Explorer в разделе Web References нужно найти ссылку на нашу веб-службу (у меня она называется ItemCardWS), щелкнуть по ней правой кнопкой мыши и выбрать пункт UpdateWebReference.

Теперь используем созданный нами метод в нашем приложении. Для начала добавим в элемент управления dataGridView1 новый столбец, который будет содержать данные о наличие на складе СИНИЙ.

Для этого изменим метод Form1_Load следующим образом:

dataGridView1.VirtualMode = true; 
dataGridView1.Columns.Insert(3, new DataGridViewTextBoxColumn()); 
dataGridView1.Columns[3].HeaderText = "BLUE Inventory"; 
dataGridView1.CellValueNeeded += new DataGridViewCellValueEventHandler(dataGridView1_CellValueNeeded);             
 
ItemCardService = new ItemCardWS.ItemCard_Service(); 
ItemCardService.UseDefaultCredentials = true;             
 
filters = new List<itemcardws.itemcard_filter></itemcardws.itemcard_filter>(); 
ItemCardWS.ItemCard_Filter descFilter = new ItemCardWS.ItemCard_Filter(); 
descFilter.Field = ItemCardWS.ItemCard_Fields.Description;

descFilter.Criteria = “*a*&*p*”;

filters.Add(descFilter); 
dataGridView1.DataSource = ItemCardService.ReadMultiple(filters.ToArray(), null, 5);

Основная часть метода осталась без изменений, единственное, что в начале были изменены некоторые свойства dataGridView1, добавлен новый столбец, а также была выполнена подписка на событие CellValueNeeded. При наступлении данного события будет вызываться метод dataGridView1_CellValueNeeded, который тоже нужно создать.

Вот этот метод:

private void dataGridView1_CellValueNeeded(object sender, DataGridViewCellValueEventArgs e) 
{ 
  if (e.ColumnIndex == 3) 
  { 
    e.Value = 0; 
    DataGridViewRow row = dataGridView1.Rows[e.RowIndex]; 
    ItemCardWS.ItemCard item = row.DataBoundItem as ItemCardWS.ItemCard; 
    if (item != null) 
    { 
      e.Value = ItemCardService.WsCalcfield(item.Key, "BLUE"); 
    } 
  } 
}

Данный метод будет вызываться для всех ячеек. Но изменять значение он будет только для нашего столбца – для идентификации используется свойство ColumnIndex. Дальше мы получаем запись соответствующую строке, для которой вызван метод. Затем происходит вызов нашего метода из программного модуля. Обратите внимание, что в качестве первого параметра ему передается bookmarkKey. Возвращаемое методом значение устанавливается в соответствующую ячейку элемента управления dataGridView1.

Выглядит все это примерно так:

Наличие на складе СИНИЙ

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

Метки:



Комментариев: 2

  1. Eduard пишет:

    Здравсвуйте Андрей,

    Не каждый способен, умеет и готов делиться своими знаниями.
    Большое Вам спасибо за это.

    С уважением,

    Эдуард

  2. Armonds пишет:

    Cперва хотел сказать большое спасибо за проделанную работу - ваша страничка мне очень помогает при изучении веб сервисов

    и 5 копеек от меня

    Следуя вашему описанию я создал 2 веб сервиса (страницу и коде унит) но назвал сервисы не ItemCard а Item Card с пробелам.

    И если веб сервис страницы работал нормально, то процедура из коде унита надбавлялась в методах. После переиминовки сервисов всё заработало.

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