异步 MVVM... 解决 WPF 中令人头疼的 GUI 冻结问题
演示如何实现 MVVM 模式下的异步模型。
引言
许多开发人员仅仅因为调用了一个执行时间很长的的同步方法,就遇到了可怕的 GUI 冻结问题。其原因是“耗时的工作”将在与 GUI 程序相同的线程上执行。这会阻止所有事件的发布和处理,从而导致用户界面被锁定,应用程序在后台工作完成之前无法使用!这确实不是交付产品的专业方式。
本站有 许多 与该主题相关的文章,其中许多都非常出色。有些很复杂,有些则需要了解解决方案的内部知识。本解决方案提供了一些不同的东西。它采用了一项较旧的技术,即 BackgroundWorker
,并添加了 ICommand
接口,在这里添加了一个委托,在那里添加了一个事件,我们就拥有了一个针对这个常见问题的完全异步且易于使用的解决方案。也许这其中最好的部分是,您可以轻松地采用这个项目并对其进行修改以满足您的需求,而无需了解太多细节。
使用代码
只需下载代码并在调试模式下运行即可看到效果。这是主屏幕
顶部的按钮是主 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
接口是 virtual
的 CanExecute
方法(默认为 true
)和 Execute
方法(同样是 virtual
),它们都旨在在具体类中被覆盖。还有一点要注意,有一个名为 (小写) canexecute
的变量。我们稍后会讨论这一点。
具体的 BaseCommand 类,名为 AsychronousCommand
如下所示,继承 BaseCommand
类,具体类首先定义了两个委托。一个是 ProgressChanged
,它接受一个类型为 int
的参数,表示进度(以百分比计)。另一个委托是 DataReady
签名,在本例中,它接受一个类型为 string
的 ObservableCollection
。这是允许任何注册的监听器接收“反馈”的初步设置。最后,基于这些委托构建了两个事件,它们将用于 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
仅仅是一个便利类,它使得传递复杂数据结构、DataTable
、List
等变得非常简单... 但请记住,您不想传递 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)……让我们看一下。
模拟工作循环
有一个循环使用 BackgroundWorkerHelper
的 SleepIteration
计数来控制循环次数,然后调用 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 会处理其余部分,最终我们会看到类似这样的结果。
附加评论
本文演示了一种轻松地启动另一个线程来执行工作,同时保持 GUI 线程不受影响的方法。它展示了如何使用 MVVM 模式,将 BackgroundWorker
和 ICommand
接口与 XAML 连接起来。AsychronusCommand(如果您还没有注意到)位于名为Model的文件夹中,这并非偶然。我们在这里有一种新的模型实现方式。它是“松耦合”的,并且在另一个线程中运行。它甚至在没有 ViewModel 引用时,也通过 Simulate.Work
类中触发的事件更新 View,这些事件由 ViewModel 处理。敏捷技术将我们带到了这一点。
历史
- 2010/11/01 - 代码格式化 (JSOP)。
- 2010/10/29 - 第一版。