【问题标题】:Is BackgroundWorker the only way to keep a WCF/WPF application responsive?BackgroundWorker 是保持 WCF/WPF 应用程序响应的唯一方法吗?
【发布时间】:2011-07-16 16:09:57
【问题描述】:

使用 C#、WCF、WPF 的客户端/服务器桌面应用程序。由于几乎每个操作都需要访问服务器(列表/创建/保存/删除/等),因此每个操作都有可能冻结整个 UI。下面是一个调用service.GetAll() 的简单实现示例,这可能需要“很长时间”(超过几百毫秒):

private void btnRefresh_Click(object sender, RoutedEventArgs e)
{
    vm.Users.Clear();
    foreach (var user in service.GetAllUsers())
        vm.Users.Add(user);
}

(旁白:我很想知道为什么ListAddRangeObservableCollection 没有。)

BackgroundWorker 救援:

private void btnRefresh_Click(object sender, RoutedEventArgs e)
{
    var worker = new BackgroundWorker();

    worker.DoWork += (s, e) =>
    {
        Dispatcher.BeginInvoke((Action)delegate() { btnRefresh.IsEnabled = false; });
        e.Result = service.GetAllUsers();
    };

    worker.RunWorkerCompleted += (s, e) =>
    {
        vm.Users.Clear();
        foreach (var user in (List<UserDto>)e.Result)
            vm.Users.Add(user);
        Dispatcher.BeginInvoke((Action)delegate() { btnRefresh.IsEnabled = true; });
    };

    worker.RunWorkerAsync();
}

(顺便说一句:上面的代码已被简化,但这就是它的要点。)

使用BackgroundWorker 的代码完全符合我的要求。该应用程序始终保持响应状态,并且该按钮在通话期间被禁用。但是,这意味着在用户可能执行的每个可能操作中添加 15 行。

说不是这样。

【问题讨论】:

  • 请确保您确实希望应用程序在这些调用期间响应。有些调用应该阻塞整个应用程序,有些调用可能会更宽松。
  • 等下一个版本的VS出来吧:msdn.microsoft.com/en-us/vstudio/async.aspx
  • @Robaticus 同意。 @John 如果可以的话。 :)

标签: c# wpf wcf backgroundworker


【解决方案1】:

不,BackgroundWorker 不是唯一的方法,而是一种方法。任何其他方式也将包括某种形式的异步构造,需要使用Dispatch.BeginInvoke 来更新 UI。例如,您可以使用ThreadPool:

ThreadPool.QueueUserWorkItem(state => {
    Dispatcher.BeginInvoke((Action)delegate() { btnRefresh.IsEnabled = false; });
    foreach (var user in service.GetAllUsers())
        vm.Users.Add(user);
    Dispatcher.BeginInvoke((Action)delegate() { btnRefresh.IsEnabled = true; });

});

如果这是一个循环模式(按钮将触发一些应该异步执行的操作,在此过程中按钮被禁用),您可以将其包装到一个方法中:

private void PerformAsync(Action action, Control triggeringControl)
{
    ThreadPool.QueueUserWorkItem(state => {
        Dispatcher.BeginInvoke((Action)delegate() { triggeringControl.IsEnabled = false; });
        action();
        Dispatcher.BeginInvoke((Action)delegate() { triggeringControl.IsEnabled = true; });     
    });
}

...并称之为:

PerformAsync(() => 
{
    foreach (var user in service.GetAllUsers())
        vm.Users.Add(user);
}, btnRefresh);

作为使用ThreadPool 的一个选项,您或许还应该查看Task Parallel Library

在执行此操作时,您应该注意处理 UI 状态的方式。例如,您有多个控件触发相同的操作,请确保在操作期间将所有控件都禁用。

注意:这些只是快速的想法。该代码未经测试,因此可能包含错误。它更多地被视为讨论材料而不是完成的解决方案。

【讨论】:

    【解决方案2】:

    WCF 提供了异步进行所有服务调用的能力。在项目中创建服务引用时,添加服务引用对话框有一个“高级...”按钮。单击它,您将看到“生成异步操作”选项。如果单击该复选框,则每个操作都将以同步和异步方式生成。

    例如,如果我有一个操作“DoSomething()”,那么在选中此框后,我将生成用于调用 DoSomething() 和 DoSomethingAsync() 的代码。

    您还将获得一个 Service.DoSomethingCompleted 事件,您可以使用该事件来定义服务调用返回时的回调处理程序。

    这是我们用来在不锁定 UI 的情况下进行服务调用的方法。

    这是微软提供的一个相当复杂的例子:http://msdn.microsoft.com/en-us/library/ms730059.aspx

    【讨论】:

      【解决方案3】:

      这不是唯一的方法。我推荐Task(或Task 的高级抽象之一,例如ParallelPLINQ)。

      我回顾了异步后台操作的各种方法on my blog

      无论您选择哪种方法,当前的情况确实需要一些样板代码。 async CTP 显示了事情的发展方向——为异步操作提供更清晰的语法。 (请注意,在撰写本文时,异步 CTP 与 VS SP1 不兼容)。

      【讨论】:

        【解决方案4】:

        嗯,BackgroundWorker 不是你唯一的选择,但为了完成你想要的,你仍然需要使用多个线程或异步操作,以便在等待长时间运行的操作完成时不会阻塞。

        而且,由于 WPF 要求访问 UI 的所有代码都在同一个线程上运行,因此当您调用或访问 UI 线程上的数据或代码时,您必须进行一些上下文切换。确保调用将在 WPF 中的 UI 线程上运行的方法是使用 Dispatcher class

        另一种保持 UI 响应的简单方法是将工作项排队到线程池中的线程上,这是使用 ThreadPool 类完成的。

        // assuming the the following code resides in a WPF control
        //  hence "this" is a reference to a WPF control which has a Dispatcher
        System.Threading.ThreadPool.QueueUserWorkItem((WaitCallback)delegate{
            // put long-running code here
        
            // get the result
        
            // now use the Dispatcher to invoke code back on the UI thread
            this.Dispatcher.Invoke(DispatcherPriority.Normal,
                     (Action)delegate(){
                          // this code would be scheduled to run on the UI
                     });
        });
        

        与往常一样,给猫剥皮的方法不止一种,但请注意,每种技术都有优点和缺点。例如,上面概述的方法可能很有用,因为它没有那么多代码开销,但在某些情况下它可能不是最有效的方法。

        其他选项可用,包括使用您正在使用的类的BeginXXX - EndXXX 方法(如果它们提供了任何方法,例如SqlCommand 类具有BeginExecuteReaderEndExecuteReader)。或者,如果类有,则使用 XXXAsync 方法。例如System.Net.Sokets.Socket 类有ReceiveAsync 和SendAsync。

        【讨论】:

          【解决方案5】:

          不,这不是唯一的选择。这个问题更多的是关于你如何设计你的应用程序。

          您可以查看Windows Composite Applicaiton Framework (Prism),它提供了诸如 EventAggregator 之类的功能,可以帮助您发布应用程序范围的事件并在应用程序内的多个位置订阅它,并据此采取行动。

          除了担心代码行数过多之外,您可能希望以一种可以重构和重用尽可能多的代码的方式对应用程序架构进行分层。这样,您可以让这些后台工作人员在一个层中处理您的所有服务响应,而您可以将 UI 层与其分离。

          【讨论】:

            【解决方案6】:

            不,这不是唯一的方法,但它是更简单的方法之一(至少与设置您自己的线程,或将任务推送到线程池线程并在完成时安排事件相比)。

            【讨论】:

              【解决方案7】:

              您可以通过在某处编写一个静态方法来简化一点,该方法接受两个参数,回调函数,并为您处理其余部分,这样您就不必每次都编写相同的样板您需要进行异步调用。

              【讨论】:

                【解决方案8】:

                不,当然不是。

                您可以创建一个原始Thread 并在其中执行耗时代码,然后将代码分派到 UI 线程以访问/更新任何 UI 控件。有关 Disptacher here 的更多信息。

                请参阅this,了解有关 c# 中线程的重要信息。

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 2014-08-10
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多