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

进度条最佳实践

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.98/5 (28投票s)

2008 年 3 月 11 日

CPOL

5分钟阅读

viewsIcon

138150

如何为长时间运行的操作显示进度通知

恰当的进度通知

这可能会引起很大的怀疑,但我还是要说出来……对于长时间操作,只有一种最佳方式来显示进度窗口。那就是在 UI 线程上显示一个模态进度窗口,同时在工作线程上执行实际工作。

首先,让我们看看为什么进度窗口必须在 UI 线程上:如果您尝试在工作线程上显示一个对话框,那么该对话框将无法拥有来自调用线程的所有者。如果您尝试这样做,您将收到异常

Cross-thread operation not valid: Control 'Form1' accessed from a thread 
other than the thread it was created on.

而在没有所有者的情况下(模态或非模态)在另一个线程上显示对话框,会使对话框看起来像非模态的,因此允许用户点击您的主窗口,从而导致进度窗口丢失到 Z 顺序的深处,使得用户难以知道操作是否仍在进行中。

您可能会尝试的一个技巧是设置进度窗口的 TopMost 属性,强制使其保持在最前面。不幸的是,这会使其始终显示在所有其他窗口之上,这对于用户试图在等待您的应用程序执行其任务时进行多任务处理来说并不理想。

接下来,让我们看看为什么工作应该在一个单独的线程上完成。如果您不这样做,并且应用程序不调用 DoEvents,那么就 Windows 而言,应用程序将很快变得无响应,并且当用户尝试点击它时,用户将在标题栏中看到“未响应”消息。我们都知道,当用户看到这个消息时,他们会觉得自己有权告诉您您的应用程序“冻结”或“卡死”。

那么为什么不使用 DoEvents 来让您的应用程序响应呢?您不仅需要频繁调用 DoEvents 来给人应用程序正在响应的假象,而且 DoEvents 是非常糟糕的。在操作过程中调用 DoEvents 会大大降低代码的可重用性。想象一下,您有一个很棒的过程可以对数字进行排序。在这种情况下,您正在锁定 UI 并显示进度条,因此 DoEvents 似乎可以达到预期效果。但请想象一下,如果有人在向导的步骤之间重用了您的排序例程。用户单击“下一步”,代码运行并调用 DoEvents,然后由于某种原因,用户决定在例程完成之前再次单击“下一步”。也许用户只是双击了,因为他们喜欢点击。按钮单击事件将再次触发,并可能给您带来各种麻烦,因为您的代码将运行两次。代码应被期望能够同步重用,而无需担心重入。如果我们都致力于这一点,那么我们的应用程序将会有更少的 bug,并且我们的代码将更容易重用。在我看来,消费者不应该担心这种细节。

最后,进度对话框应该是模态还是非模态?您可能认为这无关紧要。假设您有一些代码需要在工作线程完成后运行,那么是代码在某种“线程已完成”事件处理程序中运行,还是在进度窗口关闭后立即运行,这重要吗?在许多情况下,我承认这无关紧要。但我认为这样做是良好的实践。这是因为您的代码总是在某种程度上从事件处理程序中调用。无论是按钮单击事件还是某个对象引发的事件,除了 Sub Main 之外,您总是在某种事件处理程序中运行。如果您生成工作线程并立即返回,该事件的调用者可能会运行一些额外的代码。现在,由于您正在显示进度条,您是在向用户表明此操作尚未完成。那么,为什么您要在操作像已经完成一样返回事件呢?如果事件有多个观察者怎么办?没有任何东西可以阻止另一个模块将第二个事件处理程序添加到同一个事件中。如果您立即返回,那么该代码将在您的长时间操作完成之前运行。这可能不是问题,但可能会。

  1. 它可能会假定没有显示模态对话框,以防它需要显示自己的对话框,或者
  2. 它可能取决于您的代码在它执行任务之前是否完成。

此外,事件可能会在您完成后执行自己的操作。例如,想象一个 BeforeOpen 事件和一个 AfterOpen 事件。如果您在 BeforeOpen 中执行长时间操作并立即返回,那么 AfterOpen 将在您完成之前触发。并且该事件的消费者可能假定您已经完成了您正在做的任何事情。最后:如果事件调用者正在捕获异常。等待您的线程完成也允许您捕获您线程上的异常,并将它们冒泡回事件发布者,甚至可能适当地设置 EventArgs.Cancel 标志。到目前为止,基于所有这些原因,我断言显示进度对话框为模态是一个好习惯,除非有令人信服的理由不这样做。

那么,我们该如何做到这一切呢?虽然有不止一种方法,但在我看来,最简单的方法是使用 System.ComponentModel.BackgroundWorker 对象。下面是如何操作:

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs)_
                                                                  Handles Button1.Click
    BackgroundWorker = New System.ComponentModel.BackgroundWorker
    BackgroundWorker.RunWorkerAsync()
    FormProgress = New FormProgress
    FormProgress.ShowDialog()
End Sub

Private Sub BackgroundWorker_DoWork(ByVal sender As Object, _
       ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker.DoWork
    ' do your long operation here
End Sub

Private Sub BackgroundWorker_RunWorkerCompleted(ByVal sender As Object, _
        ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) _
        Handles BackgroundWorker.RunWorkerCompleted
    FormProgress.Close()
End Sub

瞧!就是这么简单。假设您还会通过 BackgroundWorker 对象及其关联的 ProgressChanged 事件报告完成的百分比,但也许您只会在进度窗体上显示一些重复的动画,在这种情况下,您只需要这些。

有一个警告:用户仍然可以按下 Alt+F4 或点击进度对话框右上角的红色 X(如果存在),因此您需要阻止这种情况。一个简单的解决办法是,除非是来自我们代码的请求,否则就取消窗体关闭。

Private AllowClose As Boolean = False

Private Sub FormProgress_FormClosing(ByVal sender As Object, _
            ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
    If e.CloseReason = CloseReason.UserClosing And AllowClose Then
        e.Cancel = True
    End If
End Sub

Public Sub ForceClose()
    AllowClose = True
    Me.Close()
    AllowClose = False
End Sub

只需从您的 BackgroundWorkerRunWorkerCompleted 事件调用 FormProgress.ForceClose 即可,这样就可以了。

历史

  • 2008 年 3 月 11 日:初次发布
© . All rights reserved.