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

VB.NET (WinForms) 中的反向装饰器或责任链

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.74/5 (8投票s)

2018年8月24日

CPOL

5分钟阅读

viewsIcon

12821

如何在 WinForms、VB.NET 中使用装饰器模式,支持 async/await 和/或多线程(更新于 2020 年 8 月 10 日 - 这更像是责任链模式,而不是装饰器模式)

引言

本文是我上一篇文章 VB.NET Winforms 中的装饰器模式 的延续。我喜欢使用该模式,原因都在文章中阐述了,但当我尝试进行多线程或 async 操作时,却遇到了瓶颈。问题在于我的实现方式。类被链接的方式是,最后一个类开始,然后启动前一个类,以此类推。由于最后一个类已经开始了执行,因此无法将其移动到另一个线程,并且在等待异步进程时会遇到问题。我尝试了几种不同的方法来使其工作。我找到的最佳方法是将组合方式颠倒过来。这使得它不太容易自我解释,但允许其用于多线程和异步操作。我称之为反向装饰器。

最终的示例应用程序可以在 GitHub 这里 找到。

反向装饰器

我将在此描述我们将要构建的架构。我将编写一个新的多线程应用程序,该应用程序将反向组合并向前运行。与我上一篇文章相比,它的工作方式如下:

代码

我需要将图像转换为 base64 字符串,以便在 HTML 电子邮件中使用。我将以此为借口来演示如何使用反向装饰器模式构建应用程序。我将假设您熟悉我关于装饰器模式的上一篇文章。

我将实现的这些操作如下:

  1. 获取文件路径
  2. 转换为 Base 64 字符串
  3. 长时间运行的任务(我添加此项是为了更突出地展示多线程的必要性)

这些操作的实现将非常简单。大多数操作都只有一行代码,因此我将不进行测试。我们只关注如何实现反向装饰器模式。

我创建了这个 UI,只有一个按钮和一个大的文本框,用于放置转换成文本的图像。我还添加了进度条,以便用户可以看到我们正在后台进行一些操作。

我添加了一个名为 *ConvertFileToBase64* 的类文件夹,并添加了这个 interface

Public Interface IConvertFileToBase64
  Sub RunMe(ByVal dataObj As ConvertFileToBase64Vals)
End Interface

您会注意到这个 interface 的方法是 Sub 而不是 Function。我们将修改其调用方式,并且由于我们使用值类进行状态更改,因此不需要它返回值。另外,如果它不需要返回值,那么在不同线程上同步值时出现的问题也会少一些。

我们的值类看起来像这样:

Public Class ConvertFileToBase64Vals

  Public Sub New()
    ErrObj = New ErrorObj()
  End Sub
  
  Public Property ErrObj As ErrorObj
  Public Property FilePathAndName As String
  Public Property Base64String As String

End Class

它像以前的示例一样包含 ErrObj,以及用于存储文件名和 base 64 字符串 的位置。没什么特别的。

第一个操作,我将其命名为 GetFilePath。我将实现 interface 并在构造函数中添加对该 interface 的引用。这是操作在运行时链接在一起的方式。目前,该类如下所示:

Public Class GetFilePath
  Implements IConvertFileToBase64

  Private _runMeNext As IConvertFileToBase64

  Public Sub New(ByVal runMeNext As IConvertFileToBase64)
    _runMeNext = runMeNext
  End Sub

  Public Sub RunMe(dataObj As ConvertFileToBase64Vals) Implements IConvertFileToBase64.RunMe

  End Sub

End Class

现在,我们添加一个检查,以确定是否需要运行 _runMeNext 函数。它将放在任何操作代码之后(尽管目前还没有操作代码,所以您还看不到它)。

  Public Sub RunMe(dataObj As ConvertFileToBase64Vals) Implements IConvertFileToBase64.RunMe

    If Not IsNothing(_runMeNext) Then
      _runMeNext.RunMe(dataObj)
    End If

  End Sub

请注意,没有返回值,因此我们永远不必返回到该方法。我们添加了错误检查和 try/catch 块,然后就可以添加我们的操作代码了。

    Public Sub RunMe(dataObj As ConvertFileToBase64Vals) Implements IConvertFileToBase64.RunMe

    If Not dataObj.ErrObj.HasError Then

      Try

      Catch ex As Exception
        dataObj.ErrObj.HasError = True
        dataObj.ErrObj.Message = "GetFilePath: " & ex.Message
      End Try

    End If

    If Not IsNothing(_runMeNext) Then
      _runMeNext.RunMe(dataObj)
    End If

  End Sub

我们添加操作代码并完成类。整个类如下所示:

Public Class GetFilePath
  Implements IConvertFileToBase64

  Private _runMeNext As IConvertFileToBase64

  Public Sub New(ByVal runMeNext As IConvertFileToBase64)
    _runMeNext = runMeNext

  End Sub

  Public Sub RunMe(dataObj As ConvertFileToBase64Vals) Implements IConvertFileToBase64.RunMe

    If Not dataObj.ErrObj.HasError Then

      Try

        Dim OpenFileDialog1 = New OpenFileDialog()
        If OpenFileDialog1.ShowDialog() = System.Windows.Forms.DialogResult.OK Then
          dataObj.FilePathAndName = OpenFileDialog1.FileName
        End If

      Catch ex As Exception
        dataObj.ErrObj.HasError = True
        dataObj.ErrObj.Message = "GetFilePath: " & ex.Message
      End Try

    End If

    If Not IsNothing(_runMeNext) Then
      _runMeNext.RunMe(dataObj)
    End If

  End Sub

End Class

类似地,我们添加了另外两个操作,我将它们命名为 ConvertToBase64LongRunningTask。您可以在源代码中看到它们。

我在窗体中添加了另外两个 subTurnOnWaitTurnOffWait。用于打开和关闭等待光标和进度条。此外,我还添加了 sub btnConvert_Click_FinishUp。它的工作是从文本框中获取结果并将其写入文本框。

好的,我们来添加一个操作将其移动到新线程。我将其命名为 MoveToNewThread。我们从与其他操作相同的格式开始,然后添加后台工作者功能。Microsoft 在此处提供了有关 backgroundworker 的文档:here。请查看该文档,看看我在 MoveToNewThread 类中所做的操作是否有意义。

Public Class MoveToNewThread
  Implements IConvertFileToBase64

  Private WithEvents _bgw As BackgroundWorker
  Private _nextSub As IConvertFileToBase64
  Private _callMeWhenDone As Action(Of ConvertFileToBase64Vals)

  Public Sub New(ByRef callMeWhenDone As Action(Of ConvertFileToBase64Vals), _
                 ByRef nextSub As IConvertFileToBase64)

    _callMeWhenDone = callMeWhenDone
    _nextSub = nextSub
    If IsNothing(_bgw) Then
      _bgw = New BackgroundWorker()
      _bgw.WorkerReportsProgress = True
      _bgw.WorkerSupportsCancellation = True
    End If

  End Sub 'New

  Private Sub bgw_DoWork(ByVal sender As System.Object, ByVal e As DoWorkEventArgs) _
   Handles _bgw.DoWork

    Dim locDataObj As ConvertFileToBase64Vals = TryCast(e.Argument, ConvertFileToBase64Vals)
    If IsNothing(locDataObj) Then
      Throw New System.Exception("GetServerInfo: No locDataObj passed. Ending Execution.")
      Exit Sub
    End If

    e.Result = locDataObj

    If Not IsNothing(_nextSub) Then
      _nextSub.RunMe(locDataObj)
    End If

  End Sub 'bgw_DoWork

  Private Sub bgw_RunWorkerCompleted(ByVal sender As Object, _
         ByVal e As RunWorkerCompletedEventArgs) Handles _bgw.RunWorkerCompleted

    Dim dataObj As ConvertFileToBase64Vals = TryCast(e.Result, ConvertFileToBase64Vals)

    _callMeWhenDone(dataObj)

  End Sub 'bgw_RunWorkerCompleted

  Public Sub RunMe(dataObj As ConvertFileToBase64Vals) Implements IConvertFileToBase64.RunMe

    If Not dataObj.ErrObj.HasError AndAlso Not _bgw.IsBusy Then
      _bgw.RunWorkerAsync(dataObj)
    ElseIf Not IsNothing(_nextSub) Then
      _nextSub.RunMe(dataObj)
    End If

  End Sub

End Class

请注意,在构造函数中,callMeWhenDone as Action 传递了一个接受 ConvertFileToBase64Vals 对象的​​方法。该方法在线程完成时调用,正如您在 bgw_RunWorkerCompleted 方法中所看到的。

我们需要编写我们将在构造函数中传递的方法。由于它将是 btnConvert_Click 事件的收尾部分,因此我将其命名为 btnConvert_Click_FinishUp。我们还需要它接受一个 ConvertFileToBase64Vals 作为参数。我提出的方法是这样的:

Private Sub btnConvert_Click_FinishUp(ByVal dataObj As ConvertFileToBase64Vals)

    If dataObj.ErrObj.HasError Then
      Me.txtResults.Text = "ERROR: " & dataObj.ErrObj.Message
    Else
      Me.txtResults.Text = dataObj.Base64String
    End If

    TurnOffWait()

  End Sub

现在是时候组合我们的类了。我们必须按照它们运行的顺序反向进行。所以我们得到这个:

    Dim runMe As IConvertFileToBase64 = Nothing
    runMe = New LongRunningTask(runMe)
    runMe = New ConvertToBase64String(runMe)
    runMe = New MoveToNewThread(AddressOf btnConvert_Click_FinishUp, runMe)
    runMe = New GetFilePath(runMe)

接下来,我们像这样运行它:

Dim dataObj As New ConvertFileToBase64Vals()
runMe.RunMe(dataObj)

请尝试一下。在代码中,我包含了一个 .jpg 文件,如果您需要一个示例文件来转换为 string。您也可以使用自己的文件,只是不要使用太大的图像,因为它会太大而导致永远挂起或出错。

请告诉我您对本文的看法。我是否犯了显而易见的错误,本可以使一切变得更容易?它是否有意义?我只使用了这个例子,因为它很方便,并且我希望快速写完这篇文章。如果我需要用一个更好的例子来写另一篇文章以使模式更清晰,请告诉我。这种模式也可以用于异步操作。

我也在 JavaScript 文章 here 中使用过这种反向装饰器模式。JavaScript 的某些特性使这种模式非常有效。

© . All rights reserved.