如何在与其他线程上的服务器通信时保持我的WPF UI响应?

时间:2009-09-11 16:29:21

标签: .net wpf user-interface asynchronous

我有一个wcf客户端,在与wcf服务进行一些复杂的交互之前,只需检查服务是否存活。由于连接失败的超时是一分钟,我想异步实现此检查。

这就是我所拥有的:

//Method on the main UI thread
public void DoSomeComplicatedInteraction()
{
  If(ConnectionIsActive()) DoTheComplicatedInteraction();
}


 private bool ConnectionIsActive()
    {
        //Status is a Textblock on the wpf form
        Status.Text = "Checking Connection";
        var ar = BeginConnectionIsActive();
        while (!ar.IsCompleted)
        {
            Status.Text += ".";
            Thread.Sleep(100);
        }
        EndConnectionIsActive(ar);
        //IsConnected is a boolean property
        return IsConnected;
    } 

   private IAsyncResult BeginConnectionIsActive()
    {
        //checkConnection is Func<bool> with value CheckConnection
        return checkConnection.BeginInvoke(null,checkConnection);
    }


    private void EndConnectionIsActive(IAsyncResult ar)
    {
        var result=((Func<bool>)ar.AsyncState).EndInvoke(ar);
        IsConnected = result;
    }

  private bool CheckConnection()
    {
        bool succes;
        try
        {
            //synchronous operation
            succes=client.Send(RequestToServer.GetPing()).Succes;
        }
        catch
        {
            succes = false;
        }
        return succes;
    }

这很有效。只是,当我尝试通过在服务器的Send方法中添加Thread.Sleep来模拟慢速服务器响应时,UI变得无法响应。而且,状态文本块的文本没有明显更新。 看来我需要某种Application.DoEvents方法。 或者我需要一种不同的方法吗?

编辑: 实际上,需要采用不同的方法。使用Thread.Sleep将在我们调用主UI线程时阻止UI。 以下是我解决它的方法:

 //Method on the main UI thread
public void DoSomeComplicatedInteraction()
{
  IfConnectionIsActiveDo(TheComplicatedInteraction);
}

   private void TheComplicatedInteraction()
    {...}

  private void IfConnectionIsActiveDo(Action action)
    {
        Status.Text = "Checking Connection";
        checkConnection.BeginInvoke(EndConnectionIsActive,
        new object[] { checkConnection, action });
    }

private void EndConnectionIsActive(IAsyncResult ar)
{
    var delegates = (object[]) ar.AsyncState;
    var is_active_delegate = (Func<bool>) delegates[0];
    var action = (Action) delegates[1];
    bool is_active=is_active_delegate.EndInvoke(ar);
    IsConnected = is_active;
    Dispatcher.Invoke(DispatcherPriority.Normal,
      new Action<bool>(UpdateStatusBarToReflectConnectionAttempt),is_active);
    if (is_active) action();
}

我对此并不满意:它将以前用两种方法传播的(同步)代码扩展为五种!这使得代码非常混乱......

3 个答案:

答案 0 :(得分:3)

这是因为你在循环中等待完成操作。此循环在主线程上完成,因此UI被冻结。相反,您应该将AsyncCallback传递给BeginInvoke(而不是null),以便在操作完成时通知您。

以下是我将如何实现它:

public void BeginConnectionIsActive()
{
    AsyncCallback callback = (ar) => 
    {
        bool result = checkConnection.EndInvoke(ar);
        // Do something with the result
    };
    checkConnection.BeginInvoke(callback,null);
}

答案 1 :(得分:2)

我无法从您的代码示例中确切地说明您调用它的位置,但一般情况下,您应该从不在UI线程上执行阻止工作。如您所述,这将导致UI无响应。你必须在后台完成这项工作。我的猜测是你在UI线程上直接运行ConnectionIsActive循环,这意味着在你从该方法返回之前,UI状态更新基本上会被阻止。

看起来你有正确的想法(使用异步方法),但实现似乎过于复杂。

方便的是,即使服务器实现不使用异步方法,WCF也允许您拥有异步客户端契约。合同只需要具有相同的合同名称,并专门为异步模式标记,如下例所示:

[ServiceContract(Name = "Ping")]
interface IPing
{
    [OperationContract(IsOneWay = false)]
    void Ping(string data);
}

[ServiceContract(Name = "Ping")]
interface IPingClient
{
    [OperationContract(AsyncPattern = true, IsOneWay = false)]
    IAsyncResult BeginPing(string data, AsyncCallback callback, object state);

    void EndPing(IAsyncResult result);
}

现在,在客户端,您可以使用IPingClient合同,只需拨打client.BeginPing(...)即可。当所有实际工作在后台完成时,它会立即返回,可选择在完成后回电。

您的代码可能如下所示:

void SomeCodeInUIThread()
{
    // ...
    // Do something to the UI to indicate it is
    // checking the connection...

    client.BeginPing("...some data...", this.OnPingComplete, client);
}

void OnPingComplete(IAsyncResult result)
{
    IPingClient client = (IPingClient)result.AsyncState;
    try
    {
        client.EndPing(result);
        // ...
    }
    catch (TimeoutException)
    {
        // handle error
    }

    // Operation is complete, so update the UI to indicate
    // you are done. NOTE: You are on a callback thread, so
    // make sure to Invoke back to the main form.
    // ...
}

答案 2 :(得分:1)

使用类似的东西:

public static class Run
{
    public static void InBackround<TResult>(Func<TResult> operation, Action<TResult> after)
    {
        Async(operation, after, DispatcherPriority.Background);
    }

    private static void Async<TResult>(Func<TResult> operation, Action<TResult> after,
                                               DispatcherPriority priority)
    {
        var disp = Dispatcher.CurrentDispatcher;

        operation.BeginInvoke(delegate(IAsyncResult ar)
        {
            var result = operation.EndInvoke(ar);
            disp.BeginInvoke(priority, after, result);
        }, null);
    }
}

并称之为:

Run.InBackround(CheckConnection, DoTheComplicatedInteraction);

需要一些委托,在后台线程中调用它,并在完成后返回值并在UI线程中调用'after'委托。

在上面的示例中,DoTheComplicatedInteraction应如下所示:

DoTheComplicatedInteraction(bool connected) {
    if(connected) { proceed... }  else { retry... }
}

如果您对Dispatcher感到困惑,请阅读msdn。

上的Build More Responsive Apps With The Dispatcher文章