使用进度对话框进行简单的异步后台任务处理(Silverlight 解决方案)






4.91/5 (15投票s)
用于异步运行任务和显示进度信息的、小型、通用、可重用的基础设施。
引言
本文演示了一个小型、通用且可重用的基础设施,用于异步运行任务,并允许它们以简单的方式更新其进度对话框。此解决方案适用于 Silverlight,但通过非常小的改动也可用于 Winforms 或 WPF。
背景
在进行 UI 相关编程时,我们经常需要执行后台处理,通常在工作线程上运行,因为我们不希望 UI 线程被阻塞,导致 UI 在操作期间无响应。可能使用工作线程或线程池在后台执行任务,但在许多情况下,我们希望在任务运行时显示一些进度。
问题
众所周知,我们绝不能从工作线程调用 UI 元素的任何方法或访问其属性,只能从创建这些元素的线程进行。该线程通常称为 UI 线程。(但应用程序可以有多个 UI 线程。)这是运行消息循环的线程,它花费大部分时间等待不同的消息并将其分派给相应的 UI 元素的处理程序。因此,当用户在应用程序中单击时,会生成消息并将其发布到消息队列。UI 线程获取这些消息并调用处理程序。这样,这些处理程序总是从 UI 线程调用。(从 .NET 2.0 开始,如果我们在工作线程中访问控件上的值,我们会收到一个*InvalidOperationException: Cross-thread operation not valid: Control 'TextBox1' accessed from a thread other than the thread it was created on.*)
因此,我们需要从 UI 线程调用这些 UI 更新。(封送至 UI 线程)在 WinForms 中,我们可以使用任何控件的 BeginInvoke
或 Invoke
方法(来自 ISynchronizableInvoke
接口)。它简单地接收一个任意委托和一个参数对象数组。该委托将被某种方式包装并作为消息发布到消息泵,UI 线程最终会调用它。
Control.BeginInvoke(Delegate del, object[] args);
在 Silverlight 中,我们使用以下方式执行相同的操作:
Control.Dispatcher.BeginInvoke(Delegate del, object[] args);
我推测(并期望)许多人都熟悉这些技术,但问题仍然是,这些技术需要额外的编码来定义委托并正确进行调用,即使您是从工作线程或工作线程事件进行调用,您也需要调用 BeginInvoke
并传递委托和参数。这也会与原始逻辑混合,并分散在代码各处,使代码可读性降低,显得 uglier。
解决方案
以下解决方案是为 Silverlight 制作的,但非常类似地可以移植到 .NET/Winforms。主要目的是将这些消息封送隐藏在进度对话框本身中。因此,从工作线程,我们可以直接在进度对话框上调用我们的 UI 更新方法,因为它本身会将调用封送到 UI 线程以调用适当的事件处理程序。
1. 进度对话框
DefaultProgressDlg
首先,我创建了一个名为 DefaultProgressDlg
的基对话框类,它继承自 ChildWindow
。这将是提供所有通用功能的最终基类,用于我们所有派生的进度对话框。它有一个 protected PostMessage(Delegate, object[] args)
方法,该方法接收一个带有参数的任意委托,并将其封送到 UI 线程。(如果它从 UI 线程调用,它会直接调用委托而不进行封送。)它还定义了所有对话框都将拥有的两个 public
通用操作:SetTitle(string)
和 CloseWindow()
。这些方法只是为它们对应的处理程序方法设置委托,并将它们连同参数一起传递给 PostMessage
。因此,总而言之,这些定义的 public
操作(并且只有它们)可以安全地从工作线程调用。这个 DefaultProgressDlg
实际上并没有在上面显示任何其他内容。
创建自定义进度对话框
如果我们想(而且很可能我们会)显示额外的信息,我们只需要从 DefaultProgressDlg
继承,并将我们的控件放置在对话框中(设计 XAML)。然后,我们定义我们的 public
操作来调用继承的 PostMessage
。我们还需要定义 private
消息处理程序,这些处理程序将被传递给委托,以便它们始终从 UI 线程调用,并可以执行 UI 更新操作。
public
操作(发布委托)
public void SetTitle (string text)
{
Action<string> a = new Action<string>(OnSetTitle);
PostMessage(a, text);
}
相应的 private
消息处理程序(在 UI 线程上调用)
private void OnSetTitle (string text)
{
this.txTitle.Text = text;
}
如果我们使用零个或最多四个参数的方法,最简单的方法是使用 .NET 基类库中定义的 Action()
或 Action<t>(T t)
委托重载,但在这里我们可以使用任何类型的委托,因为 PostMessage
接受 Delegate
类型,这是所有委托的基础。
2. 任务
AsyncProcess
程序集还定义了一个 abstract TaskBase<t>
类来编写您的任务。编写自定义任务时,您只需从中派生,将适当的 ProgressDialog
类类型设置为 T
泛型参数,并用自定义逻辑重写 Run abstract
方法。TaskBase
类有一个 Start()
方法,当该方法被调用时,它会创建指定的进度对话框,显示它,并在工作线程上异步启动 Run
方法。由于进度对话框实例是 TaskBase
内部的 protected
属性,您的自定义任务将始终可以访问它,并可以直接调用进度对话框上定义的 public
操作。
** 注意:只有定义的自定义操作才可安全地从非 UI 线程调用!***
创建自定义任务
public class TestTask1 : TaskBase<mycustomdialog>
{
//note this method is running on worker thread
protected override void Run()
{
ProgressDialog.SetTitle("Test1 running..");
//your logic comes here..
ProgressDialog.SetTitle("Test1 finished..");
}
protected override Dispose(bool disposing)
{
//put your cleanup code here
base.Dispose(disposing);
{
}
注意:您无需在最后调用 ProgressDialog.CloseWindow()
,因为基类会为您调用它。它还会调用指定的完成回调(稍后参见),并调用任务的 dispose。
AsyncController – 启动任务
AsyncController
是一个简单的 static
类,具有几个 static
方法,您可以在其中启动您的任务。TaskBase Start()
方法实际上设置为 internal,因此只能使用 AsyncController
启动。定义了 3 个重载:
启动任务并立即返回。未分配回调。
public static void Queue(TaskBase task)
启动任务并分配一个回调,该回调在任务完成时调用。立即返回。
public static void Queue(TaskBase task, Action<taskbase> completionCallback)
启动任务,分配一个回调,如果 wait 为 true,则调用线程将被阻塞直到任务完成。
public static void Queue(TaskBase task, Action<taskbase> completionCallback, bool wait)
以下重载接收将在运行时调用的委托。如果您没有自定义任务,只想使用一个方法作为任务,这将非常有用。委托的类型为 Action<TP>
(其中 TP
是正在使用的对话框)。这些重载构建了内部提供的 DelegateTask
实例。
public static void Queue<TP>(Action<TP> taskRun)
public static void Queue<TP>(Action<TP> taskRun, bool IsCancellable)
//...
此外,非常重要的是,您可以从任何线程启动任务!
对话框始终在 UI 线程上创建,TaskBase 类确保了这一点!。
摘要
正如您所见,这确实是一个紧凑的解决方案,用于解决上述问题。您只需要定义自定义进度对话框(编写 public
操作和 private
处理程序)和自定义任务(重写 Run()
方法),就这样。对话框创建、启动工作线程、封送委托以及其他一些功能都封装在基类中。
附加功能
取消操作
还有一种内置的方法可以允许操作被取消。正在使用的对话框和实际任务都应该支持它。
要使您的任务可取消,您应该重写 IsCancellable
属性并返回 true
。在这种情况下,当用户在对话框中取消操作时,您的任务将在 OnCancelled()
方法中收到通知。因此,您也应该重写它并将您的取消逻辑放在这里。
public class TestTask1 : TaskBase
{
private bool _stop = false;
protected override bool IsCancellable
{
get {return true;}
}
//note this method is running on worker thread
protected override void Run()
{
ProgressDialog.SetTitle("Test1 running..");
//your logic comes here..
while(!_stop)
{
//do something
}
ProgressDialog.SetTitle("Test1 finished..");
}
protected override OnCancelled()
{
ProgressDialog.SetTitle("Cancelling..");
_stop = true;
{
}
要使自定义对话框支持取消,您需要做的就是重写 CancelButton
属性并返回 System.Windows.Controls.Button
类型的实例。只需在 XAML 的任何位置放置一个 Button
并在 CancelButton
属性中返回此按钮。
注意:如果任务的 IsCancellable
设置为 false
,即使对话框支持取消,取消按钮也将设置为不可见。反之,即使任务支持取消,但对话框中未指定 CancelButton
,用户也无法取消操作。
等待操作完成
正如您所见,当您在 AsyncController
中启动任务时,您可以将 wait 标志设置为 true,这将导致调用线程等待(阻塞)直到操作完成。
** 注意:您永远不应该在 UI 线程上使用它,因为它会阻塞它。它只能在从工作线程启动任务时使用。(例如,一个正在运行的任务可以启动一个子任务并等待它完成。) **
任务完成回调
您还可以在 AsyncController
中启动任务时指定一个完成回调。这样,您可以在后台任务完成时收到通知。
** 注意:完成回调是从工作线程调用的!**
3 提供的进度对话框
除了可以显示标题的 DefaultProgressDlg
之外,*AsyncOperation.dll* 中还提供了另外 3 个预构建的对话框。
DialogOnlyStatus
它可以在窗口客户区的顶部显示一个主要状态消息。
DialogOneProgress
与
DialogOnlyStatus
相同,但还可以显示一个进度条和进度条的状态文本。DialogTwoProgress
与
DialogOnlyStatus
相同,但还可以显示两个进度条,每个进度条都有两个状态文本。
提问
在这里,我还有一个问题。
正如您所见,理论上 DialogOneProgress
应该继承自 DialogOnlyStatus
,因为它具有相同的主要状态文本以及进度条和进度状态。
DialogTwoProgress
可以从 DialogOneProgress
继承,因为它有一个额外的进度条。目前,如类图所示,所有 3 个对话框都继承自基本的 DefaultProgressDlg
,因此一些功能是重复的。
问题是我找不到继承一个也具有 XAML 的控件或窗口的方法。如果您知道方法,请告诉我!谢谢。
附加源代码
在源代码中,您可以找到 *AsyncOperation.dll* 的项目,以及一个测试 Silverlight 应用程序,该应用程序定义了一些使用预构建对话框的自定义任务。
