65.9K
CodeProject 正在变化。 阅读更多。
Home

异步 MVVM... 解决 WPF 中令人头疼的 GUI 冻结问题

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.50/5 (10投票s)

2010 年 10 月 30 日

CPOL

8分钟阅读

viewsIcon

97168

downloadIcon

1257

演示如何实现 MVVM 模式下的异步模型。

引言

许多开发人员仅仅因为调用了一个执行时间很长的的同步方法,就遇到了可怕的 GUI 冻结问题。其原因是“耗时的工作”将在与 GUI 程序相同的线程上执行。这会阻止所有事件的发布和处理,从而导致用户界面被锁定,应用程序在后台工作完成之前无法使用!这确实不是交付产品的专业方式。

本站有 许多 与该主题相关的文章,其中许多都非常出色。有些很复杂,有些则需要了解解决方案的内部知识。本解决方案提供了一些不同的东西。它采用了一项较旧的技术,即 BackgroundWorker,并添加了 ICommand 接口,在这里添加了一个委托,在那里添加了一个事件,我们就拥有了一个针对这个常见问题的完全异步且易于使用的解决方案。也许这其中最好的部分是,您可以轻松地采用这个项目并对其进行修改以满足您的需求,而无需了解太多细节。

使用代码

只需下载代码并在调试模式下运行即可看到效果。这是主屏幕

AsychronousMainWindow.png

顶部的按钮是主 TabControl 中的 TabItem。您将默认进入第一个选项卡,其中有两个按钮:“启动异步任务”和“停止异步任务”。一旦启动任务,您将在底部的进度条上看到工作状态。按下“停止”按钮,后台工作将停止。

要查看 BackgroundWorker 对主 GUI 线程的影响,只需在进度条运行时按下任何其他选项卡。您可以浏览网页,或者查看此设计如何实现的代码图。

关注点

BaseCommand 类

那么 BaseCommand 是关于什么的?它是一个实现了 ICommand 接口的 BackgroundWorker。敏捷编程技术教导我们要抽象化共性。BaseCommand 类就是这样一种尝试。

public class BaseCommand :  BackgroundWorker, ICommand
{
    public bool canexecute = true;
    public event EventHandler CanExecuteChanged;

    //------------------------------------------------------------------
    public BaseCommand()
    {
        this.WorkerSupportsCancellation = true;
        this.WorkerReportsProgress = true;
        this.DoWork += new DoWorkEventHandler(BWDoWork);
        this.ProgressChanged += 
          new ProgressChangedEventHandler(BWProgressChanged);
        this.RunWorkerCompleted += 
          new RunWorkerCompletedEventHandler(BWRunWorkerCompleted);
    }

    //------------------------------------------------------------------
    public virtual void BWRunWorkerCompleted(object sender, 
                        RunWorkerCompletedEventArgs e)
    {
    }

    //------------------------------------------------------------------
    public virtual void BWProgressChanged(object sender, 
                        ProgressChangedEventArgs e)
    {
    }

    //------------------------------------------------------------------
    public virtual void BWDoWork(object sender, DoWorkEventArgs e)
    {
    }

    //------------------------------------------------------------------
    public virtual bool CanExecute(object parameter)
    {
        return true;
    }


    //------------------------------------------------------------------
    public virtual void Execute(object parameter)
    {
    }
}

作为一个 BackgroundWorker,我们发现一些基本设置是在基类中设置的。这个工作者支持取消、报告进度以及执行工作的事件。它预先连接了 ProgressChanged 事件处理程序以及 RunWorkerCompleted 事件处理程序。但请注意,方法本身是 virtual 的。这允许具体实现覆盖这些方法并实现所需的功能。

ICommand 接口 virtualCanExecute 方法(默认为 true)和 Execute 方法(同样是 virtual),它们都旨在在具体类中被覆盖。还有一点要注意,有一个名为 (小写) canexecute 的变量。我们稍后会讨论这一点。

具体的 BaseCommand 类,名为 AsychronousCommand

如下所示,继承 BaseCommand 类,具体类首先定义了两个委托。一个是 ProgressChanged,它接受一个类型为 int 的参数,表示进度(以百分比计)。另一个委托是 DataReady 签名,在本例中,它接受一个类型为 stringObservableCollection。这是允许任何注册的监听器接收“反馈”的初步设置。最后,基于这些委托构建了两个事件,它们将用于 ViewModel 中的“松耦合”通信。

public class AsynchronusCommand : BaseCommand
{
    <summary>
    /// This is the delegate definition to post progress back to caller via the 
    /// event named EHProgressChanged
    ///<summary>
    ///<param name="progress">
    // Hold the progress integer value (from 0-100)</param>
    //-----------------------------------------------------------------------
    public delegate void DlgProgressChanged(int progress);

    //-----------------------------------------------------------------------
    /// <summary>
    /// This is the delegate definition to post
    ///   a ObservableCollection back to the caller via the
    ///   event EHDataReady
    /// </summary>
    /// <param name="data">The signature
    ///   needed for the callback method</param>
    public delegate void DlgDataReady(ObservableCollection<string> data);

    //-----------------------------------------------------------------------
    //Static event allows for wiring up to event before class is instanciated
    /// <summary>
    /// This is the Event others can subscribe to,
    /// to get the post back of Progress Changed
    /// </summary>
    public static event DlgProgressChanged EHProgressChanged;

    //-----------------------------------------------------------------------
    //Static event to wire up to prior to class instanciation
    /// <summary>
    /// This is the event of which others can
    /// subscribe to receive the data when it's ready
    /// </summary>
    public static event DlgDataReady EHDataReady; 

    //-----------------------------------------------------------------------
    /// <summary>
    /// The Entry point for a WPF Command implementation
    /// </summary>
    /// <param name="parameter">Any parameter
    /// passed in by the Commanding Architecture</param>
    public override void Execute(object parameter)
    {
        if (parameter.ToString() == "CancelJob") 
        { 
            // This is a flag that the "other thread" sees and supports
            this.CancelAsync(); 
            return;
        }
        canexecute = false; 
        this.RunWorkerAsync(GetBackGroundWorkerHelper());
    }

现在我们来到了这个类的 ICommand 部分。这是类中将通过 Execute 方法(如上所示)接收来自 WPF 中绑定命令的通知的部分。WPF 通过此演示中的 XAML 命令绑定调用 Execute 方法。该方法中的第一个检查是查看传入的参数是否是要取消任务;这只是一个任意字符串“CancelJob”。如果是,则通过所示代码取消此线程,并将控制权返回给应用程序。如果不是取消操作,则代码调用异步 RunWorkerAsync 方法(BackgroundWorker 类内置的支持)。但是等等,这个 GetBackGroundWorkerHelper() 调用是怎么回事?

BackgroundWorkerHelper

我早就发现,通过创建一个 BackgroundWorkerHelper 类,可以大大简化与其他线程的通信。它不过是一个容器,用于存放您想传递到另一个线程以及从另一个线程传递出来的任何内容。在下面的示例中,我们展示了两个对象和一个 SleepIteration 值。这是在实例化 BackgroundWorkerHelper 时设置的。BackgroundWorkerHelper 确实还有其他变量,我们稍后会看到。

//-----------------------------------------------------------------------
/// <summary>
/// A helper class that allow one to encapsulate everything
/// needed to pass into and out of the 
/// Worker thread
/// </summary>
/// <returns>BackGroundWorkerHelper</returns>
public BGWH GetBackGroundWorkerHelper() 
{
    //The BGWH class can be anything one wants it to be..
    //all of the work performed in background thread can be stored here
    //additionally any cross thread communication can
    //be passed into that background thread too.
    BGWH bgwh = new BGWH(){obj1 = 1,
                           obj2 = 2,
                           SleepIteration = 200};
    return bgwh;
}

那么 BackgroundWorkerHelper 类是什么样的?对于这个演示,我们任意设置如下。这些对象除了表明任何内容都可以传递到另一个线程之外,不做任何事情。请注意,变量 Data 是将在后台线程中填充的 ObservableCollection

//////////////////////////////////////////////////////////////////
/// <summary>
/// Background worker Class allows you to pass in as many objects as desired,
/// Just change this class to suit your needs. 
/// </summary>
public class BGWH 
{
    /// <summary>
    /// This demo chose a Object for the first "thing" to be passed in an out.
    /// </summary>
    public object obj1;  

    /// <summary>
    /// This is the second thing and shows another "object"
    /// </summary>
    public object obj2;

    /// <summary>
    /// An arbitrary integer value named SleepIteration
    /// </summary>
    public int SleepIteration;

    /// <summary>
    /// An observable collection
    /// </summary>
    public ObservableCollection<string> Data = 
           new ObservableCollection<string>(); 
}

了解 BackgroundWorkerHelper 仅仅是一个便利类,它使得传递复杂数据结构、DataTableList 等变得非常简单... 但请记住,您不想传递 GUI 对象的引用,因为您永远无法在没有特殊代码的情况下从这些线程更新它们。此外,这违反了 MVVM 的规则,因为 ViewModel 处理内容。

异步执行工作

那么“另一个线程”在哪里?BackgroundWorker 将通过前面讨论的 RunWorkerAsync 方法调用启动一个异步线程。它将调用 BWDoWork 方法,我们通过在 BaseCommand 类中注册 EventHandler 来连接它。但请注意,我们在 Concrete 类中覆盖了基类中的该方法,如下所示。

//-----------------------------------------------------------------------
/// <summary>
/// This is the implementation of the Asynchronous logic
/// </summary>
/// <param name="sender">Caller of this method</param>
/// <param name="e">The DoWorkEvent Arguments</param>
public override void BWDoWork(object sender, 
                System.ComponentModel.DoWorkEventArgs e)
{
    //Ahh! Now we are running on a separate asynchronous thread
    BGWH bgwh = e.Argument as BGWH; 
    //always good to put in validation
    if (bgwh != null)
    {
        //we are able to simulate a long outstanding work item here
        Simulate.Work(ref bgwh, this);
    }
    //All the work is done make sure to store result
    e.Result = bgwh;
}

还要注意“解包”货物有多容易。DoWorkEventArgs 包含一个参数,我们知道它是一个 BackgroundWorkerHelper。我们怎么知道?因为我们通过前面讨论的 GetBackGroundWorkerHelper() 方法调用传递了它。我们解包这个帮助类,然后检查 null,并将其作为引用传递给 Simulate.Work 方法。Simulate.Work 调用所做的只是进入一个循环,稍作等待,将数据添加到 BackgroundWorkerHelper 类中的 ObservableCollection,然后……它将一个进度事件通知发布回 View(通过 ViewModel)……让我们看一下。

模拟工作循环

有一个循环使用 BackgroundWorkerHelperSleepIteration 计数来控制循环次数,然后调用 TheAsynchCommand.ReportProgress 来报告进度,如下所示。

 //-----------------------------------------------------------------------
public static void Work( ref Model.BGWH bgwh, BaseCommand TheAsynchCommand) 
{
    //shows how the BGWH can have many different control mechanisms
    //note that we pass it in as a reference which means all updates here are 
    //automatically reflected to any object that has this reference.
    int iteration = bgwh.SleepIteration;

    //This is iterative value to determine total progress. 
    double perIteration = .005;

    //simulate reading 200 records with a small delay in each..
    Random r = new Random();

    for (int i = 0; i < iteration + 1; i++)
    {
        System.Threading.Thread.Sleep(r.Next(250));

        //Update the data element in the BackGroundWorkerHelper
        bgwh.Data.Add("This would have been the " + 
                      "data from the SQL Query etc. " + i);

        //currentstate is used to report progress
        var currentState = new ObservableCollection<string>();
        currentState.Add("The Server is busy... Iteration " + i);
        double fraction = (perIteration) * i;
        double percent = fraction * 100;

        //here a call is made to report the progress to the other thread
        TheAsynchCommand.ReportProgress((int)percent, currentState);

        //did the user want to cancel this job? If so get out.
        if (TheAsynchCommand.CancellationPending == true)
        {
            // get out of dodge
            break;
        }
    }
}

请注意,在循环底部,有一个检查以确定是否取消工作。如果为 true,我们只需跳出此循环并退出类。因为 BackgroundWorkerHelper 助手是作为引用传递的,所以数据内容已经设置好了!它可以准备好发布回 ViewModel。

回到 AsynchronousCommand 类

还记得我们连接和覆盖的那些事件处理程序吗?当 Simulate.Work 类想要报告进度时,它只需触发事件以报告进度。AsynchronousCommand 类中的事件处理程序(在 GUI 线程上运行)会接收到它,然后触发一个事件来通知 ViewModel。这通过 EHProgressChanged(progress) 信号在下面显示。

//-----------------------------------------------------------------------
/// BackGround Work Progress Changed, runs on this object's main thread
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public override void BWProgressChanged(object sender, 
       System.ComponentModel.ProgressChangedEventArgs e)
{
    //allow for a Synchronous update to the WPF Gui Layer
    int progress = e.ProgressPercentage;

    //notify others that the progress has increased.
    EHProgressChanged(progress);
    EHDataReady((ObservableCollection<string>)e.UserState);
}

EHDataReady 也在尝试在数据完成之前更新数据时被触发。测试未能确定数据是否可以在如上所示的 ProgressNotify 操作中显示。这方面还需要做更多工作。然而,正如您将在此应用程序中看到的,进度条运行完美。

有人可能会问,“我们如何阻止用户反复触发同一个命令?”还记得 BaseCommand 类中无处不在的 canexecute 变量吗?下面这个在 BackgroundThread 完成时调用的方法负责处理状态。

/// <summary>
/// Handles the completion of the background worker thread.
/// This method is running on the current thread and
/// can be used to update the execute method with information as needed
/// </summary>
/// <param name="sender">The sender of this event</param>
/// <param name="e">The Run WorkerCompleted Event Args</param>
public override void BWRunWorkerCompleted(object sender, 
       System.ComponentModel.RunWorkerCompletedEventArgs e)
{
  //ideally this method would fire an event
  //to the view model to update the data
  BGWH bgwh = e.Result as BGWH;
  var data = bgwh.Data;
  //notify others that the data has changed.
  EHDataReady(data);
  EHProgressChanged(0);
  canexecute = true;
}

Asynchronous Commands 的 BWRunWorkerCompeted 事件处理程序(已从 BaseCommand 类覆盖并预先连接以接收事件)会收到通知。它解析 BackgroundWorkerHelper,然后触发两个事件,第一个是 Data 事件(在 ViewModel 中处理),第二个是重置 ProgressBar 到零,最后,将 canexecute 设置为 true,这将允许再次调用此命令(canexecute 会阻止同一命令被多次触发)。请注意;然而,这也可以在 Execute 方法中通过检查线程是否忙碌来实现;如果忙碌,则应通过消息通知用户。当前解决方案不会让用户按按钮超过一次,这是一个不错的方法。

ViewModel

ViewModel 是如何处理这些事件的?查看 ViewModel 中的前两行代码,我们看到两个 EventHandler 被连接到了静态 Model 的 AsynchronousCommand 委托。还记得文章开头我们定义这些签名的地方吗?

//-----------------------------------------------------------------------
public MainWindowViewModel()
{
    //the model will send a notification to us when the data is ready
    Model.AsynchronusCommand.EHDataReady += 
      new Model.AsynchronusCommand.DlgDataReady(
      AsynchronusCommandEhDataReady);
    //this is the event handler to update
    //the current state of the background thread
    Model.AsynchronusCommand.EHProgressChanged += 
      new Model.AsynchronusCommand.DlgProgressChanged(
      AsynchronusCommandEhProgressChanged);

这是 ViewModel 中的方法……真的很简单,因为第一个方法只接受一个表示进度的整数,第二个方法接受一个 string 类型的 ObservableCollection

///-----------------------------------------------------------------------
// The event handler for when there's new progress to report
void AsynchronusCommandEhProgressChanged(int progress)
{
    Progress = progress;
}

//-----------------------------------------------------------------------
// The event handler for when data is ready to show to the end user
void AsynchronusCommandEhDataReady(ObservableCollection<string> data)
{
    Data = data;
}

然后,它们仅仅调用属性设置器,如下所示。WPF 会处理其余部分,最终我们会看到类似这样的结果。

The End of the Demo

附加评论

本文演示了一种轻松地启动另一个线程来执行工作,同时保持 GUI 线程不受影响的方法。它展示了如何使用 MVVM 模式,将 BackgroundWorkerICommand 接口与 XAML 连接起来。AsychronusCommand(如果您还没有注意到)位于名为Model的文件夹中,这并非偶然。我们在这里有一种新的模型实现方式。它是“松耦合”的,并且在另一个线程中运行。它甚至在没有 ViewModel 引用时,也通过 Simulate.Work 类中触发的事件更新 View,这些事件由 ViewModel 处理。敏捷技术将我们带到了这一点。

历史

  • 2010/11/01 - 代码格式化 (JSOP)。
  • 2010/10/29 - 第一版。
© . All rights reserved.