代码之家  ›  专栏  ›  技术社区  ›  Dabblernl

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

  •  1
  • Dabblernl  · 技术社区  · 15 年前

    我有一个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方法中睡眠,用户界面才会变得没有响应。此外,状态文本块的文本不会得到明显更新。 似乎我需要某种application.doEvents方法。 或者我需要一个不同的方法?

    编辑: 事实上,有必要采用不同的方法。使用thread.sleep会在我们调用主用户界面线程时阻塞用户界面。 我是这样解决的:

     //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 回复  |  直到 15 年前
        1
  •  3
  •   Thomas Levesque    15 年前

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

    以下是我将如何实现它:

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

    我不能从您的代码示例中确定您在哪里调用它,但一般来说,您应该 从未 在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.
        // ...
    }
    
        3
  •  1
  •   gimalay    15 年前

    使用类似的方法:

    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(bool connected) {
        if(connected) { proceed... }  else { retry... }
    }
    

    如果你对调度员感到困惑,请阅读 Build More Responsive Apps With The Dispatcher 关于msdn的文章。