In this post, I describe the architecture we will be building for Reverse Decorator. I write a new application, a multi-threaded one that will compose backwards and run forward.
Introduction
This article is a continuation of my last article, Decorator Pattern in VB.NET Winforms. I love using that pattern for all the reasons given in the article, but I ran up against a wall whenever I would try to do anything with multi-threading or async
. The problem is how I implemented it. The way the classes are linked, the last class begins, then starts the previous and on up the line. Because the last class has started execution, it is impossible to move it to another thread and has trouble waiting for an async process. I tried a few different ways to get it to work. The best way I found is to flip the composition upside down. It makes it a bit less self documenting, but allows it to be used in multi-threading and async operations. I call it the Reverse Decorator.
The finished example application can be found on GitHub here.
Reverse Decorator
I will describe here the architecture we will be building. I'll write a new application, a multi-threaded one that will compose backwards and run forward. In contrast to my last article, it will work like this:
The Code
I had to convert an image into the base64 string
to be used in an HTML email. I'll use that as an excuse to demonstrate how to build an application with the reverse decorator pattern. I'll assume that you are familiar with my last article on the decorator pattern.
The actions I'll be implementing are the following:
- Get File Path
- Convert To Base 64
String
- Long Running Task (I added this to make the need to be multi-threaded more pronounced)
The implementation of those actions will be trivial. Most will be one line of code, so I'm going to forgo testing. We are just focusing on how to implement the reverse decorator pattern.
I created this UI, just the button and a big text box to put the image converted to text. I also put the progress bar so that the user can see we are doing something in the background.
I added a class folder called ConvertFileToBase64 and added this interface
:
Public Interface IConvertFileToBase64
Sub RunMe(ByVal dataObj As ConvertFileToBase64Vals)
End Interface
You'll notice that this interface
's method is a Sub
and not a Function
. We will modify how it is called and because we use the value class for state changes, we don't need it to return a value. Also, less problems with syncing the values on different threads if it doesn't have to return a value.
Our value class looks like this:
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
It has the ErrObj
as in the previous example, as well as spots to hold the file name and the base 64 string
. Nothing really exciting.
The first action, I'll name GetFilePath
. I'll implement the interface
and add the reference to that interface
in the constructor. This is how the actions get linked together at run time. Currently, the class looks like this:
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
Now, we add the check to see if we need to run the _runMeNext
function. It will go after any action code (although there is no action code, so you can't see it yet).
Public Sub RunMe(dataObj As ConvertFileToBase64Vals) Implements IConvertFileToBase64.RunMe
If Not IsNothing(_runMeNext) Then
_runMeNext.RunMe(dataObj)
End If
End Sub
Notice there is no return value so we never have to return to this method. We add the error checking and a try
/catch
block and then we are ready to add our action code.
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
We add the action code and complete our class. The whole class is as follows:
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
Similarly, we add the other two actions, I named them ConvertToBase64
and LongRunningTask
. You can see them in the source code.
I added two other sub
s to the form, TurnOnWait
and TurnOffWait
. To turn on and off the wait cursor and progress bar. Further, I added the sub btnConvert_Click_FinishUp
. It has the job of getting the result and writing it to the text box.
OK, let's add an action to move this to a new thread. I'll name it MoveToNewThread
. We start with the same format we used for the other actions, then add the background worker functionality. Microsoft has documentation on the backgroundworker
here. Take a look at that documentation and see if what I did in the MoveToNewThread
class makes sense:
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
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
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
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
Notice that in the constructor, callMeWhenDone as Action
is passing a method that takes a ConvertFileToBase64Vals
object. This method is called when the thread is finished, as you see in the bgw_RunWorkerCompleted
method.
We need to write the method that we will pass in the constructor. Since it will be the finishing up part to the btnConvert_Click
event, I name it btnConvert_Click_FinishUp
. We also need it to take a ConvertFileToBase64Vals
as a parameter. This is what I came up with:
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
Now it is time to compose our classes together. We have to do it in reverse order of how we want them to run. So we have this:
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)
Next, we run it like this:
Dim dataObj As New ConvertFileToBase64Vals()
runMe.RunMe(dataObj)
Go ahead and give it a try. In the code, I've included a .jpg file if you need an example one to convert to string
. You can also use your own, only don't use too large an image as it will be too big and hang forever or throw an error.
Let me know what you think about this article. Did I make some obvious mistake that would have made everything easier? Does it make sense? I only used this example because it was handy and I wanted to write this article quickly. Let me know if I need to do another one with a better example to make the pattern more clear. This same pattern can be used for async actions too.
I've also used this reverse decorator pattern in the JavaScript article here. There are certain things about JavaScript that make this pattern work really well.