Click here to Skip to main content
13,737,754 members
Click here to Skip to main content
Add your own
alternative version

Tagged as

Stats

6.4K views
7 bookmarked
Posted 9 Aug 2018
Licenced CPOL

Decorator Pattern in VB.NET (WinForms)

, 16 Oct 2018
Rate this:
Please Sign up or sign in to vote.
Using the decorator pattern in WinForms, VB.NET

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 adding 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:

  1. Get the data from the text boxes (Due Date and Task)
  2. Write it to the DataGridView
  3. 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 interfaces. 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 textboxes 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  'AlertOnError

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.  

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Arlo Weston
United States United States
No Biography provided

You may also be interested in...

Comments and Discussions

 
QuestionThank you! and some suggestions Pin
LightTempler10-Aug-18 19:19
memberLightTempler10-Aug-18 19:19 
AnswerRe: Thank you! and some suggestions Pin
Arlo Weston14-Aug-18 18:53
memberArlo Weston14-Aug-18 18:53 
GeneralRe: Thank you! and some suggestions Pin
LightTempler15-Aug-18 9:15
memberLightTempler15-Aug-18 9:15 
GeneralRe: Thank you! and some suggestions Pin
Klaus Luedenscheidt20-Aug-18 19:17
memberKlaus Luedenscheidt20-Aug-18 19:17 
GeneralRe: Thank you! and some suggestions Pin
Arlo Weston21-Aug-18 3:26
memberArlo Weston21-Aug-18 3:26 
GeneralRe: Thank you! and some suggestions Pin
Klaus Luedenscheidt21-Aug-18 20:09
memberKlaus Luedenscheidt21-Aug-18 20:09 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web04-2016 | 2.8.180920.1 | Last Updated 16 Oct 2018
Article Copyright 2018 by Arlo Weston
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid