Асинхронный доступ к веб-службам в шаблонах проектирования .NET

ОГЛАВЛЕНИЕ

Описание реализации шаблона начало/конец (.NET 1) и событийно-управляемой модели (.NET 2).

•    Скачать демо-проект - 27.4 Кб

Введение

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

Для подключения к веб-службе можно сгенерировать класс-посредник с помощью инструмента Язык описания Веб-служб (WSDL) в .NET Framework или путем добавления веб-ссылки в Visual Studio. Посредник инкапсулирует все открытые методы, предоставляемые веб-службой, в виде синхронных и асинхронных функций. Подробней читайте в документации MSDN. Одна из имеющихся полезных статей – «Асинхронные вызовы веб-служб по HTTP с помощью .NET Framework» от Мэтта Повелла.

Асинхронная реализация преимущественно зависит от сгенерированного класса-посредника. .NET Framework предоставляет две асинхронных конструкции в посреднике. Одна – шаблон проектирования начало/конец из .NET 1.0, а вторая – событийно-управляемая модель, имеющаяся в .NET 2.0. В данной статье показаны обе реализации и описано несколько интересных и недокументированных проблем. Пример кода содержит простую веб-службу, клиент, собранный в VS 2003 для .NET 1.1, и другой клиент, собранный в VS 2005 для .NET 2.0.

Тестовая веб-служба

Так как веб-служба общая для платформы, веб-службу можно использовать независимо от ее происхождения или версии. Была создана тестовая служба в .NET 2.0, используемая обоими клиентами. Service.cs в следующем листинге-1 представляет службу с единственным методом GetStock(), принимающим символ и возвращающим его котировку.

// Листинг-1. Тестовая веб-служба в Service.cs

using System;
using System.Web.Services;
using System.Threading;

[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]

public class Service : System.Web.Services.WebService
{
    Random  _delayRandom  = new Random();
    Random  _stockRandom = new Random();

    [WebMethod]
    public double GetStock(string symbol, int timeout)
    {
        int delay = _delayRandom.Next(50, 5000);
        Thread.Sleep(delay > timeout ? timeout : delay);

        if (delay > timeout) return -1;

        double value;
        switch (symbol)
        {
            case "MSFT": value = 27;  break;
            case "ELNK": value = 11;  break;
            case "GOOG": value = 350; break;
            case "SUNW": value = 6;   break;
            case "IBM":  value = 81;  break;
            default: value = 0; break;
        }

        return value + value * 0.1 * _stockRandom.NextDouble();
    }
}

Используются два объекта Random для моделирования действия службы. _delayRandom имитирует трафик по сети с 50 до 5000 миллисекунд, а _stockRandom предназначен для колебаний значения котировки. Служба позволяет клиенту установить timeout, второй параметр GetStock(). Поэтому, помимо возврата нормальной котировки, GetStock() также возвращает ноль для нераспознанного символа и минус один в качестве флага простоя.

Для простоты служба размещена на тестовом сервере VS 2005, как показано ниже:


 
Чтобы сделать это самостоятельно, вызовите WebDev.WebServer.exe с опцией для физического пути, где Service.asmx располагается так (смотрите startWsTest.bat в демо):

C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\WebDev.WebServer.EXE
/port:1111
/path:"c:\Articles\CallWsAsync\code\WebService2"
/vpath:"/callwsasync"

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

 

Как видно, первый - статус OK с возвращенной котировкой. Второй – произошло исключение, если сервер/сеть недоступна. Третий – для неверного символа. Четвертый возникает при щелчке по кнопке Отменить сразу, чтобы прервать запрос. Последний – ответ сервера на время простоя, определенное в пользовательском интерфейсе. В каждом сценарии время сессии регистрируется на экране. Данный снимок экрана показывает пользовательский интерфейс первого клиента, и оба клиента реализованы одинаково.

Шаблон проектирования начало/конец

Чтобы создать посредник в .NET 1.1, воспользуйтесь командой Добавить веб-ссылку в VS 2003, укажите URL для виртуального пути, например: http://localhost:1111/callwsasync/, и выберите Service.asmx. Посредник содержит BeginGetStock() и EndGetStock(). Назовите его WsClient1.WsTestRef1.Service и определите объект типа _wsProxy1. Листинг-2 ниже показывает, как работает первый клиент:

// Листинг-2. Использование шаблона начало/конец с обратным вызовом

private void buttonGetQuote_Click(object sender, System.EventArgs e)
{
    textBoxResult.Text = "";
    _tmStart = DateTime.Now.Ticks;

    string symbol = comboBoxSymb.Text.ToUpper();
    int timeout = int.Parse(textBoxTimeout.Text);

    _wsProxy1 = new WsClient1.WsTestRef1.Service();
    _wsProxy1.BeginGetStock(symbol, timeout,
            new AsyncCallback(OnGetStock), symbol);   
}

private void OnGetStock(IAsyncResult ar)
{
    string symbol = (string)ar.AsyncState;
    string result;
   
    try
    {
        double value = _wsProxy1.EndGetStock(ar);
        if (value ==0)
            result = "Invalid, " + "'" +symbol +"'";
        else
        if (value <0)
            result = "TimeOut, " + "[" +symbol +"]";
        else
            result = "OK, " + value.ToString("F");
    }
    catch (WebException e)
    {
        if (e.Status == WebExceptionStatus.RequestCanceled)
            result = "Cancelled, " + "<" +symbol +">";
        else
            result = "Exception, " + e.Message;
    }
    catch (Exception e)
    {
        result = "Exception, " + e.Message;
    }

     textBoxResult.Invoke(new ShowResultDelegate(ShowResult),
                                      new object[] {result});
    _wsProxy1 = null;
}
   
private void buttonCancel_Click(object sender, System.EventArgs e)
{
    if (_wsProxy1 != null)
        _wsProxy1.Abort();
}

private delegate void ShowResultDelegate(string str);
private void ShowResult(string str)
{
    textBoxResult.Text = str + ",  (" +
      (DateTime.Now.Ticks - _tmStart) / 10000 + " ms)";
}

В buttonGetQuote_Click() получаются символ и время простоя из диалогового окна и передаются в _wsProxy1.BeginGetStock(), чтобы запустить асинхронный запрос. Третий параметр BeginGetStock() инициализирует обратный вызов OnGetStock(). Передается последний параметр AsyncState и символ, чтобы вернуть позже в обратном вызове как индикатор. Как только BeginGetStock() вызван, можно отменить запрос, вызвав _wsProxy1.Abort() в buttonCancel_Click().

Посмотрите на OnGetStock(). В блоке try сначала вызывается EndGetStock() для возврата результата. Помните, ноль означает нераспознанный символ, отрицательное значение для времени простоя, а остальные считаются нормальной котировкой.

Заметьте, что отмена ловится в WebException. Если вызывается Abort() посредника, запрос прерывается с веб-исключением. Обратный вызов все же вызывается, и WebException генерируется из EndGetStock(). Оно обнаруживается путем проверки состояния RequestCanceled, чтобы отличить другие веб-исключения, такие как сервер/сеть не работает.

Асинхронный обратный вызов выполняется в другом потоке, неявно управляемом пулом потоков .NET. Обратный вызов может не быть в контексте потока, вызывающего BeginGetStock(). Будьте сознательны, когда пытаетесь отправить команды управляющему элементу формы или обратиться к объекту-экземпляру, определенному в классе. Для этого вызывается textBoxResult.Invoke() вместо установки textBoxResult.Text напрямую.\