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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (15投票s)

2010年4月15日

CPOL

8分钟阅读

viewsIcon

50771

downloadIcon

1081

用于异步运行任务和显示进度信息的、小型、通用、可重用的基础设施。

引言

本文演示了一个小型、通用且可重用的基础设施,用于异步运行任务,并允许它们以简单的方式更新其进度对话框。此解决方案适用于 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 中,我们可以使用任何控件的 BeginInvokeInvoke 方法(来自 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

    它可以在窗口客户区的顶部显示一个主要状态消息。

    DialogOnlyStatus.png

  • DialogOneProgress

    DialogOnlyStatus 相同,但还可以显示一个进度条和进度条的状态文本。

    DialogOneProgress.png

  • DialogTwoProgress

    DialogOnlyStatus 相同,但还可以显示两个进度条,每个进度条都有两个状态文本。

    DialogTwoProgress.png

提问

在这里,我还有一个问题。

正如您所见,理论上 DialogOneProgress 应该继承自 DialogOnlyStatus,因为它具有相同的主要状态文本以及进度条和进度状态。

DialogTwoProgress 可以从 DialogOneProgress 继承,因为它有一个额外的进度条。目前,如类图所示,所有 3 个对话框都继承自基本的 DefaultProgressDlg,因此一些功能是重复的。

问题是我找不到继承一个也具有 XAML 的控件或窗口的方法。如果您知道方法,请告诉我!谢谢。

附加源代码

在源代码中,您可以找到 *AsyncOperation.dll* 的项目,以及一个测试 Silverlight 应用程序,该应用程序定义了一些使用预构建对话框的自定义任务。

classdiagram.png
© . All rights reserved.