Исходники.Ру - Программирование
Исходники
Статьи
Книги и учебники
Скрипты
Новости RSS
Магазин программиста

Главная » Статьи по программированию » .NET - Все статьи »

Обсудить на форуме Обсудить на форуме

Создание асинхронных бизнес-объектов для использования в клиентах Windows Forms .NET
создание асинхронных API для бизнес-объектов позволяет предотвратить замораживание пользовательского интерфейса клиента Microsoft Windows Forms при выполнении методов данного объекта. В статье приводится пошаговый разбор построения асинхронного API для простого бизнес-объекта и рассматривается использование вспомогательных классов для значительного упрощения процесса.

Резюме

Cоздание асинхронных API для бизнес-объектов позволяет предотвратить замораживание пользовательского интерфейса клиента Microsoft Windows Forms при выполнении методов данного объекта. В статье приводится пошаговый разбор построения асинхронного API для простого бизнес-объекта и рассматривается использование вспомогательных классов для значительного упрощения процесса. (15 печатных страниц)
Загрузите исходный код для этой статьи.

Содержание

Обзор

Если при создании приложения Microsoft® Windows® Forms имеется объект с методами, на выполнение которых требуется определенное время, можно написать асинхронный API. Предположим, что существует объект, загружающий большие файлы из удаленного местоположения. Без асинхронного API пользовательский интерфейс клиента замораживается во время вызова. Асинхронный пользовательский интерфейс клиента не замораживается. Можно даже создать асинхронный API, который дает вызывающей программе возможность получать обновления по мере выполнения процесса вызова, а клиенту - возможность отменить вызов. Некоторые печальные ситуации, такие как замороженный пользовательский интерфейс, можно отменить только с помощью диспетчера задач.

Другими словами, необходимо решать различные проблемы, возникающие при асинхронном вызове объектов и построении асинхронных API для собственных бизнес-объектов. В этой статье данные проблемы исследуются с помощью пошагового разбора построения асинхронного API для простого бизнес-объекта. Кроме того, будет разобрано использование и реализация вспомогательных классов, которые значительно упрощают задачу по реализации асинхронного API. Эти вспомогательные классы включены в код примера; их целесообразно использовать при построении собственных асинхронных API. Не следует забывать, что, несмотря на сложные детали реализации этих вспомогательных классов, их использовать достаточно просто.

Для изучения этой статьи необходимо знать Microsoft .NET Framework®, многопоточность и асинхронную модель программирования .NET.

Простой бизнес-объект

У рассматриваемого бизнес-объекта есть два метода: Method и GetNextChunk. Метод Method принимает и возвращает строку и включает 4-секундную задержку, позволяющую смоделировать длинный вызов. Метод GetNextChunk моделирует получение данных из запроса частями и тоже имеет встроенную задержку.

public struct Customer
{
   private string _FirstName;
      
   public string FirstName
   { 
      get { return _FirstName; } 
      set { _FirstName = value; } 
   }
}

public class BusinessObject
{
   public string Method(string append)
   {   
      Thread.Sleep (4000);
      return "asyncstring: " + append;
   }

   public Customer[] GetNextChunk( int chunksize )
   {
      Random r = new Random ();
      Customer[] cus = new Customer [chunksize];
      Customer c = new Customer();

      for ( int i = 0 ; i < chunksize;  i ++ )
      {
         cus[i].FirstName = r.Next(3000).ToString ();
         Thread.Sleep (200);
      }

      return cus;
   }
}

Проблема с вызовом этих API клиентом Microsoft® Windows® Forms состоит в том, что они замораживают пользовательский интерфейс на значительное время. Существуют два варианта решения этой проблемы: необходимо вынудить клиента вызывать синхронный API асинхронно через делегата на стороне клиента или реализовать асинхронный API, который мог бы быть использован для написания кода Windows Forms. В этой статье подробно рассматривается второй вариант.

Асинхронный API

Если для данного класса необходимо реализовать асинхронный API, рекомендуется следовать шаблону, используемому в .NET Framework для асинхронных API. .NET Framework делает такой метод, как GetResponse, асинхронным с помощью реализации методов BeginGetResponse и EndGetResponse. Рассмотрим в качестве примера эту выдержку из System.Net.WebRequest:

public virtual WebResponse GetResponse();

public virtual IAsyncResult BeginGetResponse(
   AsyncCallback callback,
   object state
);

public virtual WebResponse EndGetResponse(
   IAsyncResult asyncResult
);

В соответствии с шаблоном реализуем следующие 4 метода: BeginMethod, EndMethod, BeginGetNextChunk и EndGetNextChunk. Чтобы немедленно возвратить эти методы вызывающей программе, методы Method или GetNextChunk не могут быть вызваны в потоке, выполняющемся в этих асинхронных API. Вместо этого вызовы синхронного метода необходимо поставить в очередь, и обратные вызовы клиента будут отправляться другому потоку. Для этого в .NET Framework необходимо усовершенствовать класс System.Threading.ThreadPool.

Помимо этого, чтобы упростить реализацию BeginGetNextChunk и EndGetNextChunk, необходимо усовершенствовать несколько вспомогательных классов. Сначала рассмотрим бизнес-объекты асинхронного API, который реализован с этими вспомогательными классами. Затем перейдем к особенностям этих классов и рассмотрим, как они работают.

public class BusinessObjectAsync
{
   protected delegate string MethodEventHandler(string append);
   protected delegate Customer[] GetNextChunkEventHandler(int chunksize);
   
   public IAsyncResult BeginGetNextChunk( int chunksize, AsyncCallback
      callback, object state )
   {
      Aynchronizer ssi = new Asynchronizer ( callback, state );
      return ssi.BeginInvoke ( new GetNextChunkEventHandler ( 
      this.GetNextChunk ), new object [] { chunksize }  );
   }

   public Customer[] EndGetNextChunk(IAsyncResult ar)
   {
      AsynchronizerResult   asr = ( AsynchronizerResult   ) ar;
      return ( Customer[] ) asr.SynchronizeInvoke.EndInvoke ( ar);
   }
}

С помощью делегата к синхронному методу для реализации этих методов могут использоваться Asynchronizer.BeginInvoke и Asynchronizer.EndInvoke. В частности, у рассматриваемого типового бизнес-объекта есть метод GetNextChunk, принимающий integer как параметр и возвращающий массив клиентов. Поэтому объявите делегата

protected delegate Customer[] GetNextChunkEventHandler(int chunksize);

и передайте его экземпляр в Asynchronizer.BeginInvoke.

Рассмотрим вспомогательные классы, предоставившие вышеописанное простое решение, более подробно.

Классы AsyncUIHelper

 

Класс AsynchronizerResult Описание
public object AsyncState Получает определяемый пользователем объект, который квалифицирует или содержит информацию об асинхронной операции.
public WaitHandle AsyncWaitHandle Получает WaitHandle, который используется для ожидания завершения асинхронной операции.
public bool CompletedSynchronously Получает информацию о том, завершена ли асинхронная операция синхронно.
public bool IsCompleted Получает информацию о том, завершена ли асинхронная операция синхронно.
public AsynchronizerResult (Delegate method, object[] args, AsyncCallback callBack, object asyncState, ISynchronizeInvoke async, Control ctr) Конструктор, инициализирующий AsynchronizerResult с помощью делегата к синхронному методу (method); делегат, выполняющий обратный вызов клиента (callBack); состояние клиента, передаваемое назад клиенту (asyncState); метка-заполнитель для объекта Asynchronizer, создавшего AsynchronizerResult; Control для вызова Control.Invoke (ctr).
public void DoInvoke(Delegate method, object[] args) Вызывает делегата к синхронному методу с помощью Delegate.DynamicInvoke.
private void CallBackCaller() Вызывает делегата обратного вызова клиента, который был передан конструктору.
public object MethodReturnedValue Возвращаемое значение из вызова синхронного метода.

 

Класс Asynchronizer Описание
public bool InvokeRequired Получает значение, указывающее, должна ли вызывающая программа вызвать Invoke при вызове объекта, реализующего этот интерфейс.
public IAsyncResult BeginInvoke(Delegate method, object[] args) Принимает делегата к синхронному методу и ставит его в очередь на выполнение в пуле потоков. Пул потоков вызывает AsynchronizerResult.DoInvoke на выполнение.
public object EndInvoke(IAsyncResult result) Получает значение из вызова синхронного метода с помощью проверки AsynchronizerResult.ReturnedValue.
public object Invoke(Delegate method, object[] args) Синхронно вызывает делегата к синхронному методу с помощью Delegate.DynamicInvoke.
public Asynchronizer(AsyncCallback callBack, object asyncState) Конструктор, который инициализирует объект с помощью делегата, выполняющего обратный вызов клиента (callBack); состояние клиента, возвращаемое клиенту (asyncState).
public Asynchronizer(Control control, AsyncCallback callBack, object asyncState) Конструктор, который инициализирует Asynchronizer с помощью Control для вызова Control.Invoke (control); делегат, выполняющий обратный вызов клиента (callBack); состояние клиента, возвращаемое клиенту (asyncState).

 

Класс Util Описание
public static void InvokeDelegateOnCorrectThread (Delegate d, object[] args) Проверяет свойство Target делегата, и если оно является подклассом Control, делегат вызывается с помощью Control.Invoke.

Как взаимодействуют Asynchronizer и AsynchronizerResult

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

public Asynchronizer( AsyncCallback callBack, object asyncState)
{
   asyncCallBack = callBack;
   state = asyncState;
}

Чтобы поставить в очередь вызовы синхронного метода, Asynchronizer.BeginInvoke использует AsychronizerResult и устанавливает делегата обратного вызова клиента на выполнение при завершении этого метода. Вызов к Asynchronizer.DoInvoke ставится в очередь с помощью класса .NET Framework ThreadPool.QueueUserWorkItem.

public IAsyncResult BeginInvoke(Delegate method, object[] args)
{
   AsynchronizerResult result = new AsynchronizerResult ( method, args,  
      asyncCallBack, state, this, cntrl );
   WaitCallback callBack = new WaitCallback ( result.DoInvoke );
   ThreadPool.QueueUserWorkItem ( callBack ) ;
   return result;
}

AsychronizerResult.DoInvoke вызывает синхронный метод бизнес-объекта и затем выполняет обратный вызов клиента с помощью CallBackCaller().

public void DoInvoke(Delegate method, object[] args) 
{
   returnValue = method.DynamicInvoke(args);
   canCancel = false;
   evnt.Set();
   completed = true;
   CallBackCaller();
}

Asychronizer.EndInvoke получает значение из вызова синхронного метода путем проверки AsynchronizerResult.MethodReturnedValue. Возвращаемое значение из этого метода является возвращаемым значением из вызова.

public object EndInvoke(IAsyncResult result)
{
   AsynchronizerResult asynchResult = (AsynchronizerResult) result;
   asynchResult.AsyncWaitHandle.WaitOne();
   return asynchResult.MethodReturnedValue;
}

Использование этих вспомогательных классов

Как упоминалось ранее, разработчику бизнес-объекта, реализующему асинхронный API, необходимо реализовать BeginMethod и EndMethod для любого метода, который нужно предоставить асинхронно. Для достижения этой цели можно делегировать работу вспомогательным методам Asynchronizer.BeginInvoke и Asychronizer.EndInvoke. Здесь снова приводится рассмотренный выше пример реализации BeginGetNextChunk и EndGetNextChunk. Теперь необходимо понять, как взаимодействуют Asynchronizer и AsynchronizerResult для обеспечения этой прямой реализации.

public IAsyncResult BeginGetNextChunk( int chunksize, 
   AsyncCallback callback, object state )
{
   Asynchronizer ssi = new Asynchronizer ( callback, state );
   return ssi.BeginInvoke ( new GetNextChunkEventHandler (
            this.GetNextChunk ), new object [] { chunksize }  );
}

public Customer[] EndGetNextChunk(IAsyncResult ar)
{
   AsynchronizerResult   asr = ( AsynchronizerResult   ) ar;
   return ( Customer[] ) asr.SynchronizeInvoke.EndInvoke ( ar);
}

Как клиент Windows Forms использует асинхронный API бизнес-объекта

Рассмотрим это в действии на стороне клиента. Ниже показано, как клиент Windows Forms может использовать BeginGetNextChunk и EndGetNextChunk.

private void btnAsync_Click(object sender, System.EventArgs e)
{
   AsyncUIBusinessLayer.BusinessObjectAsync bo = new
      AsyncUIBusinessLayer.BusinessObjectAsync ();
   CurrentAsyncResult = bo.BeginGetNextChunk (20, new AsyncCallback (
      this.ChunkReceived ), bo );
   this.statusBar1.Text = "Request Sent";
}

public void ChunkReceived (IAsyncResult ar)
{
   this.label2.Text = "Callback thread = " +
      Thread.CurrentThread.GetHashCode ();
   BusinessObjectAsync bo = (BusinessObjectAsync  ) ar.AsyncState ;
   Customer [] cus =  bo.EndGetNextChunk( ar );
   this.dataGrid1.DataSource = cus;
   this.dataGrid1.Refresh ();
   this.statusBar1.Text = "Request Finished";
}

Несмотря на внешнюю безупречность, здесь скрыта неожиданная проблема. Обратный вызов из пула потоков клиента Windows Forms происходит в потоке из пула, а не в потоке Windows Forms. Этот способ выполнения обратных вызовов Windows Form не поддерживается. Фактически единственными методами элемента управления, которые можно вызвать в другом потоке, являются Invoke, BeginInvoke, EndInvoke и CreateGraphics.

Один подход состоит в решении проблемы полностью разработчиком клиента. Разработчику необходимо лишь знать, как вызвать другие методы Control через Control.Invoke в обработчиках обратных вызовов. Ниже приводится пример реализации безопасного обратного вызова: из клиентского вызова BeginGetNextChunk. Обратный вызов ChunkReceivedCallbackSafe немедленно использует Control.Invoke, чтобы выполнить любой код, обновляющий пользовательский интерфейс.

public void UpdateGrid (AsyncUIBusinessLayer.Customer[] cus)
{
   this.lblCallback.Text = "Callback thread = " +
      Thread.CurrentThread.GetHashCode ();
   this.dataGrid1.DataSource = cus;
   this.dataGrid1.Refresh ();
   this.statusBar1.Text = "Request Finished";
}

public void ChunkReceivedCallbackSafe(IAsyncResult ar)
{
   this.lblCallback.Text = "Callback thread = " +   
      Thread.CurrentThread.GetHashCode ();
   AsyncUIBusinessLayer.BusinessObjectAsync bo = 
      (AsyncUIBusinessLayer.BusinessObjectAsync ) ar.AsyncState ;
   AsyncUIBusinessLayer.Customer [] cus =  bo.EndGetNextChunk( ar );
   if (this.InvokeRequired )
   {
      this.Invoke( new delUpdateGrid (this.UpdateGrid ), new object[] 
         {cus});
   }
   else
   {
      UpdateGrid (cus);
   }
}

Обеспечение безопасных обратных вызовов бизнес-объекта при вызове из элементов управления

Класс, реализующий асинхронный API, способен выполнять больше работы, упрощая тем самым работу разработчика клиента. В этой статье будут рассмотрены два подхода к выполнению этого:
  1. Включение Control в качестве параметра для этого асинхронного API. Любые обратные вызовы Control проходят через Control.Invoke.
  2. Исследование класса Delegate.Target, в котором хранится объект, имеющий обратный вызов. Если это Control, обратный вызов выполняется через Control.Invoke.

Передача Control бизнес-объекту

Эти добавления к Asynchronizer позволяют так передать ему Control, чтобы обратный вызов мог проходить через этот метод вызова элемента управления. Добавим параметр к конструктору Asynchronizer, который принимает элемент управления как ввод. Это будет элемент управления, вызывающий Control.Invoke. Кроме того, необходимо изменить BeginGetNextChunk так, чтобы передать этот элемент управления. Для этого реализуем BeginGetNextChunkOnUIThread так, чтобы первым параметром был элемент управления, в котором выполняется обратный вызов.

//Business Object
private IAsyncResult BeginGetNextChunkOnUIThread( Control control, 
   int chunksize, AsyncCallback callback, object state )
{
   Asynchronizer ssi = new Asynchronizer ( control, callback, state);
   return ssi.BeginInvoke ( new GetNextChunkEventHandler 
      (this.GetNextChunk ), new Object [] { chunksize }  );
}

private Customer[] EndGetNextChunkOnUIThread(IAsyncResult ar)
{
   return base.EndGetNextChunk  ( ar ) ;
}

//Asynchronizer
public Asynchronizer( Control control, AsyncCallback callBack, object 
   asyncState)
{
   asyncCallBack = callBack;
   state = asyncState;
   cntrl = control;
}

//AsynchronizerResult
private void CallBackCaller()
{   
   if ( resultCancel == false )
   {
      if (onControlThread)
      {
         cntrl.Invoke ( asyncCallBack, new object [] { this } );
      }
      else
      {
         asyncCallBack ( this );      
      }
   }
}

Ниже приводится пример клиента Windows Forms, использующего этот API путем передачи Windows Forms указателя this классу BeginGetNextChunkOnUIThread:

private void btnAsyncOnUIThread_Click (object sender, System.EventArgs e)
{
  BusinessObjectAsync bo = new BusinessObjectAsync ();
  CurrentAsyncResult = bo.BeginGetNextChunkOnUIThread (this, 20, 
      new AsyncCallback (this.ChunkReceived ), bo );
  this.statusBar1.Text = "Request Sent";
}

public void ChunkReceived (IAsyncResult ar)
{
   this.lblCallback.Text = "Callback thread = " + 
      Thread.CurrentThread.GetHashCode ();
   AsyncUIBusinessLayer.BusinessObjectAsync bo =
      (AsyncUIBusinessLayer.BusinessObjectAsync  ) ar.AsyncState ;
   AsyncUIBusinessLayer.Customer [] cus =  bo.EndGetNextChunk( ar );
   this.dataGrid1.DataSource = cus;
   this.dataGrid1.Refresh ();
   this.statusBar1.Text = "Request Finished";
}

Использование Delegate.Target для проверки того, находится ли обратный вызов в элементе управления Windows Forms

К сожалению, BeginGetNextChunkOnUIThread по сравнению с BeginGetNextChunk, который может использоваться клиентами, отличными от Windows Forms, более сложен для разработчика клиента, который должен не забывать использовать этот API.

Для этого существует более удобный способ. Воспользуемся тем, что в любом делегате имеется свойство Target. Это свойство хранит целевой объект для делегата. Поэтому можно проверить это свойство и определить, действительно ли обратный вызов делегата происходит в Windows Forms на основе того, является ли свойство Target подклассом Control. Это выполняется следующим образом:

public Asynchronizer( AsyncCallback callBack, object asyncState)
{
   asyncCallBack = callBack;
   state = asyncState;
   if (callBack.Target.GetType().IsSubclassOf         
      (typeof(System.Windows.Forms.Control)))
   {
      cntrl = (Control) callBack.Target ;
   }
}

При использовании этого метода клиент не несет нагрузки по передаче элемента управления, на котором он выполняется, бизнес-объекту. Следовательно, передавать Windows Forms указатель this при вызове BeginNextChunk не требуется.

private void btnAsyncOnUI_Click(object sender, System.EventArgs e)
{
   AsyncUIBusinessLayer.BusinessObjectAsync bo = new 
      AsyncUIBusinessLayer.BusinessObjectAsync ();
   CurrentAsyncResult = bo.BeginGetNextChunk (20, new AsyncCallback ( 
      this.ChunkReceived ), bo );
   this.statusBar1.Text = "Request Sent";
}

Использование компонентов для упрощения кода обратного вызова клиента

В упрощении задачи реализации асинхронного API был достигнут значительный прогресс, однако недостатком вышеупомянутой модели является то, что разработчик клиента должен знать шаблоны асинхронного программирования .NET. Разработчики должны быть знакомы с моделями BeginMethod и EndMethod и с использованием IAsyncResult.

Альтернативный асинхронный API можно предоставить классу с помощью событий. К рассматриваемому типовому бизнес-классу можно добавить событие GetNextChunkCompleteEvent. Это позволяет не передавать обратный вызов вызову асинхронного метода. Вместо этого клиент добавляет и удаляет обработчиков для этого события. Ниже приводится этот новый API для бизнес-объекта:

public event GetNextChunkComponentEventHandler GetNextChunkCompleteEvent;

public void GetNextChunkAsync(int chunksize, object state )
{
   if (GetNextChunkCompleteEvent==null)
   {
      throw new Exception ("Need to register event for callback.  
         bo.GetNextChunkEventHandler += new 
         GetNextChunkComponentEventHandler (this.ChunkReceived );");
   }
   GetNextChunkState gState = new GetNextChunkState ();
   gState.State = state;
   gState.BO = bo;
   bo.BeginGetNextChunk (chunksize, new AsyncCallback (
      this.ChunkReceived ), gState);
} 

private void ChunkReceived( IAsyncResult ar)
{
   GetNextChunkState gState = (GetNextChunkState) ar.AsyncState ;
   AsyncUIBusinessLayer.BusinessObjectAsync  b = gState.BO ;
   Customer[] cus = b.EndGetNextChunk (ar);
   AsyncUIHelper.Util.InvokeDelegateOnCorrectThread (
      GetNextChunkCompleteEvent, new object[] { this, new
      GetNextChunkEventArgs ( cus, gState.State) });
}

Большое преимущество этого API состоит в том, что если бизнес-объект является компонентом, обработчики событий клиента могут быть установлены через пользовательский интерфейс. Если перетащить компонент бизнес-объекта на поверхность для проектирования клиента (см. рис. 1), то можно выбрать свойства этого компонента и установить с их помощью обработчиков событий (см. рис. 2 ниже).

Рис. 1. Перетащите компонент на форму

Рис. 2. Установите обработчики событий компонента

Поскольку EndGetNextChunk уже не вызывается для получения результатов вызова метода, на стороне клиента используется GetNextChunkEventArgs, который имеет свойство Customers для получения массива клиентов.

private void btnAsyncThroughComponent2_Click(object sender, 
   System.EventArgs e)
{
   businessObjectComponent1.GetNextChunkAsync (15, this.dataGrid2 );
   this.statusBar1.Text = "Request Sent";
}

private void businessObjectComponent1_GetNextChunkComplete(object sender, 
   BusinessObjectComponent.GetNextChunkEventArgs args)
{
   this.lblCallback.Text = "Callback thread = " + 
      Thread.CurrentThread.GetHashCode ();
   AsyncUIBusinessLayer.Customer[] cus = args.Customers ;
   DataGrid grid = (DataGrid) args.State ;
   grid.DataSource = cus;
   grid.Refresh ();
}

Заключение

Замораживания пользовательского интерфейса клиента при выполнении методов бизнес-объекта можно избежать с помощью построения асинхронных API для объектов. Хотя для построения собственного решения нужно приложить некоторые усилия, эту задачу можно существенно упростить с помощью вспомогательных классов AsynchronizerResult, Asynchronizer и Util, доступных в коде примера и рассмотренных в этой статье. Результат для клиентов (в отсутствии боязни) стоит приложенных усилий.


Может пригодится:


Автор: Курт Шенк
Прочитано: 4783
Рейтинг:
Оценить: 1 2 3 4 5

Комментарии: (0)

Добавить комментарий
Ваше имя*:
Ваш email:
URL Вашего сайта:
Ваш комментарий*:
Код безопастности*:

Рассылка новостей
Рейтинги
© 2007, Программирование Исходники.Ру