Introduction
Decorator pattern in VB.NET, WinForms! Here is the GitHub repo. In this article, I try and use the benefits we get from WinForms and from SOLID principles.
Background
This is my implementation of a Pluralsight course I took a while back, the course is “Encapsulation and SOLID”, by Mark Seemann. It is a very good course and I would highly recommend it. In this design, I also try and follow the KISS principle.
Decorator
The decorator pattern "decorates" an object with added actions without modifying the object or other objects of the same class. I will implement it starting with an interface and then add each action as a class that implements that interface. Additionally, I will link the classes together with a linked list like structure. Each class will take an interface in its constructor. At run time, when running its action, it will first check to see if it has an action it was sent in the constructor. If so, it will run it first. This structure can be used to link together as many actions as needed. Here is a graphic:
Using the Code
Start a new WinForm project in Visual Studio, call it ToDoSample
, and save it. Add a Unit Test Project to the WinForm app (File – Add – New Project …) call it ToDoSampleTests
.
Design the UI
Winforms gives us a good drag and drop interface to help us design our UI. So we first leverage that ability to quickly design it. Go ahead and put the title, text boxes, buttons, dataGridView
, etc. on Form1
.
Great! UI done! Now we need it to do something. Here is where the SOLID principles come into play. First principle, "single responsibility principle". Each class should have only a single responsibility. We think about what we want the program to do when we click the "Add" button. We break up all these things into individual single responsibilities. This is open to some individual interpretation, but this is what I came up with:
- Get the data from the text boxes (Due Date and Task)
- Write it to the
DataGridView
- Sort
DataGridView
by due date
This will guide us in creating functions around these actions. We'll put them each into their own classes. This allows us to test them all independently and it is easy to understand when we come back in six months to add new features.
The Set Up
The way we design the classes becomes key. We follow more of the SOLID principles. The next three principles work together: open/closed principle, Liskov substitution principle, and interface segregation principle.
In addition to the three principles above, I'll be working towards putting everything together with composition. That will allow the functions to be very separate but able to seamlessly be put together at run time. Near as I can tell, using composition this way is called the decorator pattern.
Unrelated to the SOLID principles, but as convenience, I'll put the actions in the same class folder. I'll call it "AddToDos". We right click on the project, select "Add New Item" - Class, name it "AddToDos
".
The first thing we add is the interface
, "IAddToDos
". After the interface
, we add a value class
, named "AddToDoVals
". The value class
is what I came up with to persist state between the composable classes. It now looks like this:
Public Interface IAddToDos
End Interface
Public Class AddToDoVals
End Class
All of our classes will implement the interface
. That is what gives them the ability to be composed together. So we add the function "RunMe
" to the interface
. RunMe
needs to have “AddToDoVals
” as a parameter as that is where the state is preserved from action to action. So we add to the interface
like this:
Public Interface IAddToDos
Function RunMe(ByVal dataObj As AddToDosVals) As AddToDosVals
End Interface
We are done with the Interface
. Interface
segregation principle says we need small and specific interface
s. So there we go.
Now to work on the value class. I'm not an expert on the Liskov substitution principle. I'll admit that I find it a bit confusing. But from what I can understand, we need to make each class so no matter what the class does, it will start and end the same. Each class needs to be robust. To that end, the first thing we put into our AddToDosVals
value class is something to tell us if there has been an error. We create the class below and add it to AddToDosVals
.
Public Class ErrorObj
Public Sub New()
HasError = False
End Sub
Public Property HasError As Boolean
Public Property Message As String
End Class
Public Class AddToDosVals
Public Sub New()
ErrObj = New ErrorObj()
End Sub
Public Property ErrObj As ErrorObj
End Class
The error class gives us a consistent way to handle the errors. Consistent error handling tells us no matter what the class does, if there has been an error, it will act the same. That goes a long way to making our code more robust and modular.
Functions Start Here
Now we are ready to write the code for the functions. Start with the first one, GetDataFromTextBoxes
. So we add a new class, and have it implement the interface
.
Public Class GetDataFromTextBoxes
Implements IAddToDos
Public Function RunMe(dataObj As AddToDosVals) As AddToDosVals Implements IAddToDos.RunMe
Throw New NotImplementedException()
End Function
End Class
We need to add the place where the data from the textbox
es will persist, so we add properties to our AddToDoVals
class.
Public Class AddToDosVals
Public Sub New()
ErrObj = New ErrorObj()
End Sub
Public Property ErrObj As ErrorObj
Public Property DueDate As Date
Public Property ToDoTask As String
End Class
To facilitate linking these functions together in a composable way, we add the IAddToDos
interface as an input to the constructor and as a private
field.
Private _runMeFirst As IAddToDos
Public Sub New(ByRef runMeFirst As IAddToDos)
_runMeFirst = runMeFirst
End Sub
Next, we want to pass everything this class depends on, and is available when we "new" it up, in the constructor. For the last SOLID principle, dependency inversion, we would pass an interface to how we connect to a database or similar. In this simple example, we will do this so anyone who comes along later will know this class is dependent on that data or functionality. We add dueDate
and toDo
parameters to the constructor and as private
fields.
Private _dueDate As Date
Private _toDo As String
Private _runMeFirst As IAddToDos
Public Sub New(ByVal dueDate As Date, ByVal toDo As String, ByRef runMeFirst As IAddToDos)
_dueDate = dueDate
_toDo = toDo
_runMeFirst = runMeFirst
End Sub
Now the RunMe
function. It will, of course, return the dataObj
, so we add it. In addition, we need it to check for any previous functions and run them before it runs the current one. This is a piece of the composability. We can handle it like this:
Public Function RunMe(dataObj As AddToDosVals) As AddToDosVals Implements IAddToDos.RunMe
If Not IsNothing(_runMeFirst) Then
dataObj = _runMeFirst.RunMe(dataObj)
End If
Return dataObj
End Function
Our function now checks the ErrObj
to make sure no previous functions have errors. In most cases, why run this function if there was an error previously? Just skip the code, and return the dataObj
so it can report the error. So we add the following check:
If Not dataObj.ErrObj.HasError Then
End If
The boilerplate code is set up now. Time for the logic. Hopefully, this isn't too simplistic an example. In this case, all we really need to do is validate the values and put them into the dataObj
so the downstream functions can use them. On validation fail, we turn on the error. The finished RunMe
function is this:
Public Function RunMe(dataObj As AddToDosVals) As AddToDosVals Implements IAddToDos.RunMe
If Not IsNothing(_runMeFirst) Then
dataObj = _runMeFirst.RunMe(dataObj)
End If
If Not dataObj.ErrObj.HasError Then
If Not _dueDate = Nothing AndAlso Not _toDo.Trim = String.Empty Then
dataObj.DueDate = _dueDate
dataObj.ToDoTask = _toDo
Else
dataObj.ErrObj.HasError = True
dataObj.ErrObj.Message = "Invalid input values"
End If
End If
Return dataObj
End Function
We now write the tests. (Some people would argue we should write the tests first, have it fail, then write the logic. I've done it both ways in practice, so whichever you prefer.)
This is actually the first time we have instantiated this type of class. It may seem a little strange, but hang in there and by the end, you'll see how writing the class this way allows us to compose these separate functions together.
Imports ToDoSample
<testclass()> Public Class AddToDosTests
<testmethod()> Public Sub GetFromTextBoxesTests()
Dim expectedDate As Date = Date.Today()
Dim expectedToDo As String = "This is my test ToDo!"
Dim addToDo As IAddToDos = Nothing
addToDo = New GetDataFromTextBoxes(expectedDate, expectedToDo, addToDo)
Dim dataObj As New AddToDosVals() addToDo.RunMe(dataObj)
Dim actualDate As Date = dataObj.DueDate
Dim actualToDo As String = dataObj.ToDoTask
Assert.AreEqual(expectedDate, actualDate)
Assert.AreEqual(expectedToDo, actualToDo)
End Sub
End Class
Woo Hoo! It works! In real life, you can add more tests to test that function. Like add some sad paths. But for this article, we will move on. We do the same thing for the next two functions. We follow the same pattern. Set up the classes, add the boilerplate, the logic, then test. You can check out how I did it in the source code.
Decorate the Interface With All the Functions!
Time to compose all these classes together. I put the composition in the button on-click event. That way, in six months, when I come back to add a feature or when some other developer does it, it is easy to reason about what is going on and what needs to be done to make the change.
When composing, there are two parts. The composition, or putting it all together, and then actually running the code. This is how we compose the classes together:
Dim addToDo As IAddToDos = Nothing
addToDo = New GetDataFromTextBoxes(Me.DateTimePicker1.Value, Me.TextBox2.Text, addToDo)
addToDo = New WriteToDataGridView(Me.dgvToDo, addToDo)
addToDo = New SortDataGridViewByDate(Me.dgvToDo, addToDo)
And then to run it, we first have to create the data object that will store any state, then we run it:
Dim dataObj As New AddToDosVals()
addToDo.RunMe(dataObj)
Run it, and it works!
Add More Features!
So we have the application, and it runs, but like any application, it isn’t done. Normally, as soon as it gets into users hands, they want additions, tweaks, changes, especially if they use it. The only sure thing is that there will be changes. No problem! This is why we use the decorator pattern. We can add functionality without having to touch the other code or the other tests. For an example, let’s notify the UI when there has been an error. We follow the same pattern as above, we add the class, add the boilerplate code, the logic, and test. I'll call the class "AlertOnError
".
First, we add a label to the form, set the ForColor
to Red
, and remove the text. We will call this label “lblError
”. Add some properties on the form to access the text.
We add the new class “AlertOnError
”. It uses the same structure of the other classes. We will need to update Form1
, so we add that as a parameter. Here is the class without any logic:
Public Class AlertOnError
Implements IAddToDos
Private _currForm As Form1
Private _runMeFirst As IAddToDos
Public Sub New(ByRef currForm As Form1, ByRef runMeFirst As IAddToDos)
_currForm = currForm
_runMeFirst = runMeFirst
End Sub
Public Function RunMe(dataObj As AddToDosVals) As AddToDosVals Implements IAddToDos.RunMe
If Not IsNothing(_runMeFirst) Then
dataObj = _runMeFirst.RunMe(dataObj)
End If
Return dataObj
End Function
End Class
We add the simple logic:
If dataObj.ErrObj.HasError Then
_currForm.ErrorMessage = "ERROR: " & dataObj.ErrObj.Message
Else
_currForm.ErrorMessage = ""
End If
Then the test:
<testmethod()> Public Sub AlertOnErrorTest()
Dim frmTest As New Form1
Dim expected As String = "ERROR: Test Error Message"
Dim addToDo As IAddToDos = Nothing
addToDo = New AlertOnError(frmTest, addToDo)
Dim dataObj As New AddToDosVals()
dataObj.ErrObj.HasError = True
dataObj.ErrObj.Message = "Test Error Message"
addToDo.RunMe(dataObj)
Dim actual As String = frmTest.ErrorMessage
Assert.AreEqual(expected, actual)
End Sub
Everything tests out, so we then add it to our decorator button logic:
Private Sub btnAdd_Click(sender As Object, e As EventArgs) Handles btnAdd.Click
Dim addToDo As IAddToDos = Nothing
addToDo = New GetDataFromTextBoxes(Me.DateTimePicker1.Value, Me.TextBox2.Text, addToDo)
addToDo = New WriteToDataGridView(Me.dgvToDo, addToDo)
addToDo = New SortDataGridViewByDate(Me.dgvToDo, addToDo)
addToDo = New AlertOnError(Me, addToDo)
Dim dataObj As New AddToDosVals()
addToDo.RunMe(dataObj)
End Sub
So there you have it. We added a feature without having to refactor any previous code or tests. It felt just like writing the code for a new project. It is also self-documenting.
Try it out. Let me know what you think. I have posted a first draft of the version that works with multi-thread and async. It is here. I'll see if I can post the one about JavaScript sometime.