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

ОГЛАВЛЕНИЕ

Событийно-управляемая модель посредника

Она может показаться сложной при получении посредника в .NET 2.0 путем выполнения того же самого в VS 2005. Назовите этот посредник WsClient2.WsTestRef2.Service и определите объект как _wsProxy2. Подобно BeginGetStock() в .NET 1.1, этот посредник предоставляет GetStockAsync() как пусковой метод. Но надо добавить обработчик события, действующий как предыдущий обратный вызов. Следующий Листинг-3 показывает код второго клиента:

// Листинг-3. Использование событийно-управляемой модели

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

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

    _wsProxy2 = new WsClient2.WsTestRef2.Service();
    _wsProxy2.GetStockCompleted += new
       GetStockCompletedEventHandler(OnGetStockCompleted);
    _wsProxy2.GetStockAsync(symbol, timeout, symbol);
}

private void OnGetStockCompleted(Object sender,
             GetStockCompletedEventArgs gca)
{
    string symbol = (string)gca.UserState;
    string result;

    if (gca.Cancelled)  // Вызывается CancelAsync
        result = "Cancelled2, " + gca.UserState;
    else
    if (gca.Error != null)
    {
        WebException webEx = gca.Error as WebException;
        if (webEx !=null && webEx.Status ==
                        WebExceptionStatus.RequestCanceled)
            result = "Cancelled, " + "<" + symbol + ">";
        else
            result = "Exception, " + gca.Error.Message;
    }
    else
    if (gca.Result == 0)
        result = "Invalid, " + "'" + symbol + "'";
    else
    if (gca.Result < 0)
        result = "TimeOut, " + "[" + symbol + "]";
    else
        result = "OK, " + gca.Result.ToString("F");

    textBoxResult.Text = result + ",  (" +
        (DateTime.Now.Ticks - _tmStart) / 10000 + " ms)";
    _wsProxy2 = null;
}

private void buttonCancel_Click(object sender, EventArgs e)
{
    if (_wsProxy2 != null)
    {
        //_wsProxy2.CancelAsync("<<" +
        //        comboBoxSymb.Text.ToUpper() + ">>");
        _wsProxy2.Abort();
    }
}

В buttonGetQuote_Click() добавляется обработчик события OnGetStockCompleted() в _wsProxy2 и вызывается GetStockAsync() для запуска асинхронного запроса. Аналогично, первые два параметра – символ и время простоя, а третий UserState, подобный AsyncState в BeginGetStock(), возвращается позже в обработчике. Запрос можно отменить с помощью последующего _wsProxy2.Abort().

Что нового в OnGetStockCompleted()? При вызове его второй параметр gca типа GetStockCompletedEventArgs (кратко смотрите его в листинге-4) приносит всю готовую информацию. gca содержит 4 свойства: UserState типа object, Result типа double, Error типа Exception и флаг Cancelled. Сравните с логикой в обратном вызове (Листинг-2), большинство частей понятны и нет нужды повторять.

Единственная хитрость касается gca.Cancelled. Как видно в Листинге-3, этот флаг сознательно проверяется в начале OnGetStockCompleted() и помещается другая проверка в состояние RequestCanceled из gca.Error. Что именно было захвачено? Определенно, успех - gca.Error, а не gca.Cancelled, потому что вызов _wsProxy2.Abort() приводит к генерации WebException.

Что если надо перехватить gca.Cancelled как флаг отмены? Немного изменим посредник. Чтобы отличить отмененный запрос от gca.Error, отображается «Отменено2» для gca.Cancelled следующим образом:

 

Используется CancelAsync() посредника, изначально ничего не делающий, но вызывающий его базовый. Поэтому в buttonCancel_Click() (в Листинге-3) вызывается _wsProxy2.CancelAsync(), а не _wsProxy2.Abort().

Листинг-4 ниже показывает измененный посредник, куда добавлено 4 нумерованных комментария, чтобы указать изменения:

// Листинг-4. Измененный событийно-управляемый посредник

public partial class Service : SoapHttpClientProtocol
{
    private System.Threading.SendOrPostCallback
                         GetStockOperationCompleted;
    private bool useDefaultCredentialsSetExplicitly;

    // 1. Добавлен управляющий флаг _done
    private bool _done = true;

    public Service() { ... }
    public new string Url { ... }
    public new bool UseDefaultCredentials { ... }
   
    public event GetStockCompletedEventHandler GetStockCompleted;

    [System.Web.Services.Protocols.SoapDocumentMethodAttribute(...)]
    public double GetStock(string symbol, int timeout)
    {
        object[] results = Invoke("GetStock",
             new object[] {symbol, timeout});
        return ((double)(results[0]));
    }

    ... ... ...
   
    public void GetStockAsync(string symbol, int timeout, object userState)
    {
        // 2. Инициализируется _dene – не готово
        _done = false;

        if (GetStockOperationCompleted == null)
        {
            GetStockOperationCompleted = new
              System.Threading.SendOrPostCallback(
              OnGetStockOperationCompleted);
        }
       
        this.InvokeAsync("GetStock", new object[] {symbol, timeout},
            GetStockOperationCompleted, userState);
    }
   
    private void OnGetStockOperationCompleted(object arg)
    {
        // 3. Когда завершено без отмены, возбуждается событие
        if (GetStockCompleted != null && !_done)
        {
            _done = true;
            InvokeCompletedEventArgs invokeArgs = (InvokeCompletedEventArgs)(arg);
            GetStockCompleted(this,
                   new GetStockCompletedEventArgs(invokeArgs.Results,
                                                  invokeArgs.Error,
                                                  invokeArgs.Cancelled,
                                                  invokeArgs.UserState));
        }
    }
   
    public new void CancelAsync(object userState)
    {
        // 4. Если готово, обработка не идет
        if (_done) return;

        // Вызвана отмена. Готово и возбуждается событие
        _done = true;
        GetStockCompleted(this,
                   new GetStockCompletedEventArgs(null,
                                                  null,
                                                  true,
                                                  userState));
        // base.CancelAsync(userState);
    }
   
    private bool IsLocalFileSystemWebService(string url) { ... }
}

[System.CodeDom.Compiler.GeneratedCodeAttribute( ... )]
public delegate void GetStockCompletedEventHandler(object sender,
                     GetStockCompletedEventArgs e);
... ...
public partial class GetStockCompletedEventArgs
     : System.ComponentModel.AsyncCompletedEventArgs
{
    private object[] results;
    internal GetStockCompletedEventArgs(object[] results,
                                        System.Exception exception,
                                        bool cancelled,
                                        object userState) :
            base(exception, cancelled, userState) { ... }
   
    public double Result { get { ... } }
}

Для ясности опущено большинство неважных участков. Во-первых, определяется флаг _done в классе и устанавливается в true, так как нет обрабатывающихся запросов. Во-вторых, в GetStockAsync(),_done инициализируется в false (не готово). GetStockAsync() регистрирует свой собственный внутренний обратный вызов OnGetStockOperationCompleted() и запускает асинхронный запрос с помощью InvokeAsync(). Как только OnGetStockOperationCompleted() вызывается для завершенного запроса, он возбуждает событие GetStockCompleted(), вызывающее обработчик OnGetStockCompleted(), добавленный ранее в _wsProxy2.

Затем, третье изменение происходит в OnGetStockOperationCompleted(). Добавляется проверка _done, чтобы запретить возбуждение, если _done уже установлен в true в CancelAsync(), что является следующим четвертым изменением. Как только пользователь вызывает CancelAsync() для отмены, если запрос завис (_done равен false), _done устанавливается в true и возбуждается событие отмены – событие отправляет аргумент GetStockCompletedEventArgs с флагом Cancelled, установленным в true.

Приведенный пример помогает понять, как работает событийно-управляемый посредник. Но в реальном производственном сценарии такое изменение посредника не рекомендуется. Если посредник восстанавливается позже, любые изменения кода будут потеряны. Поэтому желательно использовать Abort() вместо CancelAsync().