6
\$\begingroup\$

The Task

A task I've been given recently is to design a data entry form which transfers data from the form to a table on a worksheet.
Depending on the answers given to various questions, other questions are displayed or hidden. For the most part I've achieved this by using different forms.
There is one route that requires a single form to ask too many questions to sit comfortably on the screen, so on my first build I figured out what I needed and am now working on various refactoring exercises.

What the code does

The class I've written handles the positioning and visibility of frames on the form. Frames added at design time can be moved, hidden or shown without ending up with empty spaces in the middle of the form. It only handles a single column of frames, so haven't added code where two frames might have the same Top value.When the class is initialised it makes a dictionary of all top level (that have the form as a parent) frames on the form.

Set FrameSorter = New cFrameSorterFrameSorter.Initialise Me

These frames can then be removed or added to the form, where they'll appear beneath the last visible frame. Their position on the form can be moved up, down or to a specified position.

FrameSorter.AddFrame Me.Frame1FrameSorter.MoveUp Me.Frame1FrameSorter.Move Me.Frame1, 2FrameSorter.Remove Me.Frame1

Review

Open to all suggested improvements. Naming conventions, order of procedures, sub, function or property? Should I use an interface (never used one before), any ideas on how to have a subclass looking at frames within frames?
I know I haven't included any error handling yet - it's a first draft, so wanted to see all errors and handle the ones I could.

The Code

To use the code:

  • Create a class namedcFrameSorter
  • Create a userform and add these controls:
    • a combobox namedcmbFrames
    • a texbox namedtxtPosition
    • 5 command buttons named:cmdMoveFrame,cmdShowFrame,cmdHideFrame,cmdMoveFrameUp andcmdMoveFrameDown.
    • A few frames. The names don't matter and frames within frames will be ignored. Have a few visible and a few not.

When the form opens it will populate the combo box with a list of frames - select a frame and click show/hide/up/down or add a number to the text box and click move.

Add this code to the class module:

Option Explicit'FrameDictionary contains all frames that have the form as the parent.'VisibleFrames contain all frames within FrameDictionary that have a'True Visible property in the order they appear.Private FrameDictionary As DictionaryPrivate VisibleFrames As DictionaryPrivate pStartPosition As LongPrivate pSpacer As LongPrivate Sub Class_Initialize()    Set FrameDictionary = New Dictionary    Set VisibleFrames = New Dictionary    Me.StartPosition = 6    Me.Spacer = 10End Sub'The position of the first frame on the form.Public Property Get StartPosition() As Long    StartPosition = pStartPositionEnd PropertyPublic Property Let StartPosition(Value As Long)    pStartPosition = IIf(Value >= 0, Value, 0)End Property'This is the distance between frames.Public Property Get Spacer() As Long    Spacer = pSpacerEnd PropertyPublic Property Let Spacer(Value As Long)    pSpacer = IIf(Value >= 0, Value, 0)End Property'This property would not normally exist.'It exists to populate the combo box on the UserForm.Public Property Get FrameDict() As Dictionary    Set FrameDict = FrameDictionaryEnd Property'Adds a frame to the VisibleFrames dictionary providing'it exists within the FrameDictionary. The frames Visible'property is set to TRUE and it will appear beneath'the last visible frame.Public Sub AddFrame(SourceFrame As Frame)    If Not SourceFrame Is Nothing Then        If FrameDictionary.Exists(SourceFrame.Name) Then            With SourceFrame                If Not VisibleFrames.Exists(.Name) Then                    .Visible = True                    VisibleFrames.Add .Name, SourceFrame                    ArrangeFrames                End If            End With        End If    End IfEnd Sub'The frame is removed from the VisibleFrames dictionary.'The frames Visible property is set to FALSE and the'remaining visible frames are rearranged to close any'gaps left.Public Sub RemoveFrame(SourceFrame As Frame)    If Not SourceFrame Is Nothing Then        With SourceFrame            If VisibleFrames.Exists(.Name) Then                .Visible = False                VisibleFrames.Remove (.Name)                ArrangeFrames            End If        End With    End IfEnd SubPublic Sub MoveUp(SourceFrame As Frame, Optional Position As Long = 1)    Dim lPosition As Long    lPosition = GetPositionInDict(SourceFrame)    If lPosition > 1 Then        MoveFrame SourceFrame, lPosition - 1        ArrangeFrames    End IfEnd SubPublic Sub MoveDown(SourceFrame As Frame, Optional Position As Long = 1)    Dim lPosition As Long    lPosition = GetPositionInDict(SourceFrame)    If lPosition > 0 And lPosition < VisibleFrames.Count Then        MoveFrame SourceFrame, lPosition + 1        ArrangeFrames    End IfEnd SubPublic Sub Move(SourceFrame As Frame, Position As Long)    MoveFrame SourceFrame, Position    ArrangeFramesEnd Sub'Looks at each frame on the SourceForm.  Any that have'the form as a parent rather than another frame is added'to the FrameDictionary.  These represent the top level frames.''As frames are looked at in the order they were added to the'form the FrameDictionary is sorted using the Top property of'each frame.''Any frames with a TRUE visible property at design time are'added to the VisibleFrames dictionary and are displayed in'order when the form first opens.Public Sub Initialise(SourceForm As Object)    Dim ctrl As Control    Dim tmpSubSorter As cFrameSorter    Dim vSortArray As Variant    For Each ctrl In SourceForm.Controls        If TypeName(ctrl) = "Frame" Then            Select Case TypeName(ctrl.Parent)                Case TypeName(SourceForm)                    With FrameDictionary                        If Not .Exists(ctrl.Name) Then                            .Add ctrl.Name, ctrl                        End If                    End With                Case "Frame"                    'Do nothing yet.            End Select        End If    Next ctrl    'Sort the frames contained in the dictionary into    'order based on their Top property.    vSortArray = FrameDictToArray(FrameDictionary)    Sort2DArray vSortArray    SortDictByArray vSortArray, FrameDictionary    'Create a dictionary of visible frames and then    'arrange them on the form in order.    GetVisibleFrames    ArrangeFramesEnd Sub'Returns the ordinal position of a frame within the VisibleFrames dictionary.'If the frame doesn't exist within the dictionary -1 is returned.Private Function GetPositionInDict(SourceFrame As Frame) As Long    Dim vItem As Variant    Dim x As Long    If Not SourceFrame Is Nothing Then        If VisibleFrames.Exists(SourceFrame.Name) Then            For Each vItem In VisibleFrames.Items                x = x + 1                If SourceFrame.Name = vItem.Name Then                    GetPositionInDict = x                    Exit For                End If            Next vItem        Else            GetPositionInDict = -1        End If    End IfEnd Function'Populates the VisibleFrames dictionary with frames'from the FrameDictionary that have a TRUE visible property.Private Sub GetVisibleFrames()    Dim tmpDict As Dictionary    Dim vItem As Variant    If Not FrameDictionary Is Nothing Then        If FrameDictionary.Count > 0 Then            Set tmpDict = New Dictionary            For Each vItem In FrameDictionary.Items                If vItem.Visible Then                    tmpDict.Add vItem.Name, vItem                End If            Next vItem        End If    End If    Set VisibleFrames = tmpDictEnd Sub'Moves a frames position within the VisibleFrames dictionary,'to a specified position.'If the required position is higher or lower than the number'of frames then the highest or lowest value is used.Private Sub MoveFrame(SourceFrame As Frame, Position As Long)    Dim tmpDict As Dictionary    Dim vItem As Variant    Dim x As Long    If Not SourceFrame Is Nothing Then        Set tmpDict = New Dictionary        SourceFrame.Visible = True        If Not VisibleFrames.Exists(SourceFrame.Name) Then            VisibleFrames.Add SourceFrame.Name, SourceFrame        End If        If Position > VisibleFrames.Count Then            Position = VisibleFrames.Count        ElseIf Position < 0 Then            Position = 0        End If        If Position = VisibleFrames.Count Then            VisibleFrames.Remove SourceFrame.Name            VisibleFrames.Add SourceFrame.Name, SourceFrame        Else            VisibleFrames.Remove SourceFrame.Name            For x = 0 To VisibleFrames.Count - 1                If x = Position - 1 Then                    tmpDict.Add SourceFrame.Name, SourceFrame                End If                tmpDict.Add VisibleFrames.Items(x).Name, VisibleFrames.Items(x)            Next x            Set VisibleFrames = tmpDict        End If    End IfEnd Sub'Positions the frames contained within the VisibleFrames dictionary on the'parent form in the order they occur within the dictionary.Private Sub ArrangeFrames()    Dim vItem As Variant    Dim lTopRow As Long    If Not VisibleFrames Is Nothing Then        If VisibleFrames.Count > 0 Then            lTopRow = Me.StartPosition            For Each vItem In VisibleFrames.Items                vItem.Top = lTopRow                lTopRow = lTopRow + vItem.Height + Me.Spacer            Next vItem        End If    End IfEnd Sub'Sorts TargetDict dictionary in the order of the array.'The vSortArray holds the frame namesPrivate Sub SortDictByArray(vSortArray As Variant, TargetDict As Dictionary)    Dim tmpDict As Dictionary    Dim vItem As Variant    Dim x As Long    If Not TargetDict Is Nothing Then        If UBound(vSortArray) = TargetDict.Count - 1 Then            Set tmpDict = New Dictionary            For x = LBound(vSortArray) To UBound(vSortArray)                tmpDict.Add vSortArray(x, 1), TargetDict.Item(vSortArray(x, 1))            Next x            Set TargetDict = tmpDict        End If    End IfEnd Sub'Takes the frame Top property and frame name to create'an array from the SourceDictionary items.Private Function FrameDictToArray(SourceDict As Dictionary) As Variant    Dim tmpDict As Dictionary    Dim x As Long    Dim tmpArr As Variant    Dim itm As Variant    If Not SourceDict Is Nothing Then        If SourceDict.Count > 0 Then            Set tmpDict = New Dictionary            ReDim tmpArr(0 To SourceDict.Count - 1, 0 To 1)            For Each itm In SourceDict.Items                tmpArr(x, 0) = itm.Top                tmpArr(x, 1) = itm.Name                x = x + 1            Next itm            FrameDictToArray = tmpArr        End If    End IfEnd Function'Sorts the array using the frames Top property.Private Sub Sort2DArray(vArray As Variant, _    Optional ByVal lLowStart As Long = -1, _    Optional ByVal lHighStart As Long = -1)    Dim vPivot As Variant    Dim lLow As Long    Dim lHigh As Long    lLowStart = IIf(lLowStart = -1, LBound(vArray), lLowStart)    lHighStart = IIf(lHighStart = -1, UBound(vArray), lHighStart)    lLow = lLowStart    lHigh = lHighStart    vPivot = vArray((lLowStart + lHighStart) \ 2, 0)    While lLow <= lHigh        While (vArray(lLow, 0) < vPivot And lLow < lHighStart)            lLow = lLow + 1        Wend        While (vPivot < vArray(lHigh, 0) And lHigh > lLowStart)            lHigh = lHigh - 1        Wend        If (lLow <= lHigh) Then            Swap vArray, lLow, lHigh            lLow = lLow + 1            lHigh = lHigh - 1        End If    Wend    If (lLowStart < lHigh) Then        Sort2DArray vArray, lLowStart, lHigh    End If    If (lLow < lHighStart) Then        Sort2DArray vArray, lLow, lHighStart    End IfEnd SubPrivate Sub Swap(vArray As Variant, lItem1 As Long, lItem2 As Long)    Dim vTemp0 As Variant    Dim vTemp1 As Variant    vTemp0 = vArray(lItem1, 0)    vTemp1 = vArray(lItem1, 1)    vArray(lItem1, 0) = vArray(lItem2, 0)    vArray(lItem1, 1) = vArray(lItem2, 1)    vArray(lItem2, 0) = vTemp0    vArray(lItem2, 1) = vTemp1End Sub

Add this code to the form:

Option ExplicitPrivate FrameSorter As cFrameSorterPrivate Sub UserForm_Initialize()    Dim vItem As Variant    Set FrameSorter = New cFrameSorter    FrameSorter.Initialise Me    'Populate the combobox.    For Each vItem In FrameSorter.FrameDict.Items        Me.cmbFrames.AddItem vItem.Name    Next vItemEnd SubPrivate Sub cmdHideFrame_Click()    FrameSorter.RemoveFrame Me.Controls(Me.cmbFrames.Value)End SubPrivate Sub cmdMoveFrame_Click()    FrameSorter.Move Me.Controls(Me.cmbFrames.Value), CLng(Me.txtPosition)End SubPrivate Sub cmdMoveFrameDown_Click()    FrameSorter.MoveDown Me.Controls(Me.cmbFrames.Value)End SubPrivate Sub cmdMoveFrameUp_Click()    FrameSorter.MoveUp Me.Controls(Me.cmbFrames.Value)End SubPrivate Sub cmdShowFrame_Click()    FrameSorter.AddFrame Me.Controls(Me.cmbFrames.Value)End Sub
askedFeb 2, 2020 at 3:16
Darren Bartrup-Cook's user avatar
\$\endgroup\$
4
  • \$\begingroup\$ReadAbout Class Modules and the other post and your knowledge will increase (try the addin too). Alsoriptutorial.com shows why to avoid Hungarian Notation (yours just one char but usually unwanted) and many other tipps!.\$\endgroup\$CommentedFeb 6, 2020 at 1:47
  • 1
    \$\begingroup\$Thanks for your feedback @ComputerVersteher. I'd love to install RubberDuck, but I only have my work laptop at the moment and they won't let me - hopefully that might change soon. I do get the point with naming variables after what they're used for, not what data type they are. I guess that's just a bad habit which I'm trying to get out of - I'm down to one character per variable. Bit like smoking I guess - I'm down to one a day on that too. :)\$\endgroup\$CommentedFeb 6, 2020 at 8:51
  • \$\begingroup\$Have read the posts on Interfaces, etc.?\$\endgroup\$CommentedFeb 6, 2020 at 9:36
  • \$\begingroup\$As no reviews (but votes, so basics match) till now, you can visit the rubberduck war room on chat (just search for rd) and leave a link. That may cause one of the knowing ones to notice your qoestion and review (my knowledge is not sufficent at now, as I am just reading through the blog and try to understand, when to use an interface)\$\endgroup\$CommentedFeb 6, 2020 at 10:07

1 Answer1

4
\$\begingroup\$

The UserForm implemented here is specifically designed to demonstrate the CFrameSorter class functions. In doing so, the UI fulfills two roles: The CFrameSorter command initiator, and results viewer. In the actual system where the CFrameSorter is used, the CFrameSorter would most likely be commanded to Move and Hide Frames by a component other than the UI. That is a communication sequence something like:

Application object(s)issue Frame manipulation commands ==> CFrameSorter(issues frame position and visibility commands) ==> FrameDisplayUI (View) places and shows Frames in response to CFrameSorter input.

In the above sequence, the UI does not issue commands going right to left. The UI in this post (because it is a CFrameSorter tester/demonstrator) is playing the roles of both the Application and View. To prepare the CFrameSorter for use in your final system, making the visual test tool (the UserForm) better simulate the interactions described above is the theme for the following review.

The primary comment is this: The UI in the final design should be completely unaware of the concrete object(s) that are manipulating it. This is also the goal of the TestUI. Currently, when the UserForm is first created, UserForm_Initialize is called. The first thing it does is:

Set FrameSorter = New CFrameSorterFrameSorter.Initialise Me

If these two commands were described in terms of human relationships, it would be the same as the UserForm telling the CFrameSorter, "I know who you are and everything about you. You are more than a member variable to me. You...complete me". In this scenario, interfaces is probaby the best way for the CFrameSorter to break out of this relationship "and still be friends".

We want to remove any awareness of the CFrameSorter class from the View..and, unltimately, any awareness of the View, from the CFrameSorter. "FrameSorter.Initialise Me" has to go. We want to do this for a few reasons, but the primary reason is that in the final system, the CFrameSorter will not be taking frame positioning commands from the UI. It will be issued commands from one or more application objects. The simplest way to set this up here is to create a StandardModule (FrameSorterTester). It's job is to simulate the Application. It will create a CFrameSorter instance as well as the View instance. Add an entry point to initiate the testing.

Sub TestCFrameSorter()    Dim frameSorter As CFrameSorter    Set frameSorter = New CFrameSorter    Dim testView As TestFrameSorterView    Set testView = New TestFrameSorterView    Load testView    testView.ShowEnd Sub

So, how to wire up the system if TestFrameSorterView is not to know anything about CFrameSorter class. Answer: Interfaces. Every VBA module that has Public subroutines, functions, or properties defines an interface. The interface is fundamentally a set of methods that define interactions. The implicit interface of CFrameSorter is:

    Public Property Get StartPosition() As Long    End Property    Public Property Let StartPosition(Value As Long)    End Property    Public Property Get Spacer() As Long    End Property    Public Property Let Spacer(Value As Long)    End Property    Public Property Get FrameDict() As Dictionary    End Property    Public Sub AddFrame(SourceFrame As Frame)    End Sub    Public Sub RemoveFrame(SourceFrame As Frame)    End Sub    Public Sub MoveUp(SourceFrame As Frame, Optional Position As Long = 1)    End Sub    Public Sub MoveDown(SourceFrame As Frame, Optional Position As Long = 1)    End Sub    Public Sub Move(SourceFrame As Frame, Position As Long)    End Sub    Public Sub Initialise(SourceForm As Object)    End Sub

As you can see, all that I've done is copied Public methods from CFrameSorter and deleted everything else. Now, create a new ClassModule "IFrameSorter" with the above empty methods in it...you've just created an interface. When an object (any object) 'implements' the IFrameSorter interface, it MUST provide logic behind every method of the interface - even if it is to raise an error that says "Public Sub Move not implemented" (for example). To 'force' CFrameSorter to implement IFrameSorter you add "Implements IFrameSorter" at the top of the CFrameSorter class module. This defines a set of methods that CFrameSorter MUST implement (it already has the logic). A simple search on 'Implement an Interface in Excel VBA' will provide the rest of the details to get to the following version of CFrameSorter:

    Option Explicit    Implements IFrameSorter    Private FrameDictionary As Dictionary    Private VisibleFrames As Dictionary    Private pStartPosition As Long    Private pSpacer As Long    Private Sub Class_Initialize()        Set FrameDictionary = New Dictionary        Set VisibleFrames = New Dictionary        pStartPosition = 6        pSpacer = 10    End Sub    Private Property Let IFrameSorter_Spacer(RHS As Long)        pSpacer = RHS    End Property    Private Property Get IFrameSorter_Spacer() As Long        IFrameSorter_Spacer = pSpacer    End Property    Private Property Let IFrameSorter_StartPosition(RHS As Long)        pStartPosition = RHS    End Property    Private Property Get IFrameSorter_StartPosition() As Long        IFrameSorter_StartPosition = pStartPosition    End Property    Private Property Get IFrameSorter_FrameDict() As Scripting.IDictionary        Set IFrameSorter_FrameDict = FrameDictionary    End Property    Private Sub IFrameSorter_AddFrame(SourceFrame As MSForms.IOptionFrame)        If Not SourceFrame Is Nothing Then            If FrameDictionary.Exists(SourceFrame.Name) Then                With SourceFrame                    If Not VisibleFrames.Exists(.Name) Then                        .Visible = True                        VisibleFrames.Add .Name, SourceFrame                        ArrangeFrames                    End If                End With            End If        End If    End Sub    Private Sub IFrameSorter_RemoveFrame(SourceFrame As MSForms.IOptionFrame)        If Not SourceFrame Is Nothing Then            With SourceFrame                If VisibleFrames.Exists(.Name) Then                    .Visible = False                    VisibleFrames.Remove (.Name)                    ArrangeFrames                End If            End With        End If    End Sub    Private Sub IFrameSorter_MoveUp(SourceFrame As MSForms.IOptionFrame, Optional Position As Long = 1&)        Dim lPosition As Long        lPosition = GetPositionInDict(SourceFrame)        If lPosition > 1 Then            MoveFrame SourceFrame, lPosition - 1            ArrangeFrames        End If    End Sub    Private Sub IFrameSorter_Move(SourceFrame As MSForms.IOptionFrame, Position As Long)        MoveFrame SourceFrame, Position        ArrangeFrames    End Sub    Private Sub IFrameSorter_MoveDown(SourceFrame As MSForms.IOptionFrame, Optional Position As Long = 1&)        Dim lPosition As Long        lPosition = GetPositionInDict(SourceFrame)        If lPosition > 0 And lPosition < VisibleFrames.Count Then            MoveFrame SourceFrame, lPosition + 1            ArrangeFrames        End If    End Sub    Private Sub IFrameSorter_Initialise(SourceForm As Object)        Dim ctrl As Control        Dim tmpSubSorter As CFrameSorter        Dim vSortArray As Variant        For Each ctrl In SourceForm.Controls            If TypeName(ctrl) = "Frame" Then                Select Case TypeName(ctrl.Parent)                    Case TypeName(SourceForm)                        With FrameDictionary                            If Not .Exists(ctrl.Name) Then                                .Add ctrl.Name, ctrl                            End If                        End With                    Case "Frame"                        'Do nothing yet.                End Select            End If        Next ctrl        'Sort the frames contained in the dictionary into        'order based on their Top property.        vSortArray = FrameDictToArray(FrameDictionary)        Sort2DArray vSortArray        SortDictByArray vSortArray, FrameDictionary        'Create a dictionary of visible frames and then        'arrange them on the form in order.        GetVisibleFrames        ArrangeFrames    End Sub    Private Function GetPositionInDict(SourceFrame As Frame) As Long        Dim vItem As Variant        Dim x As Long        If Not SourceFrame Is Nothing Then            If VisibleFrames.Exists(SourceFrame.Name) Then                For Each vItem In VisibleFrames.Items                    x = x + 1                    If SourceFrame.Name = vItem.Name Then                        GetPositionInDict = x                        Exit For                    End If                Next vItem            Else                GetPositionInDict = -1            End If        End If    End Function    Private Sub GetVisibleFrames()        Dim tmpDict As Dictionary        Dim vItem As Variant        If Not FrameDictionary Is Nothing Then            If FrameDictionary.Count > 0 Then                Set tmpDict = New Dictionary                For Each vItem In FrameDictionary.Items                    If vItem.Visible Then                        tmpDict.Add vItem.Name, vItem                    End If                Next vItem            End If        End If        Set VisibleFrames = tmpDict    End Sub    Private Sub MoveFrame(SourceFrame As Frame, Position As Long)        Dim tmpDict As Dictionary        Dim vItem As Variant        Dim x As Long        If Not SourceFrame Is Nothing Then            Set tmpDict = New Dictionary            SourceFrame.Visible = True            If Not VisibleFrames.Exists(SourceFrame.Name) Then                VisibleFrames.Add SourceFrame.Name, SourceFrame            End If            If Position > VisibleFrames.Count Then                Position = VisibleFrames.Count            ElseIf Position < 0 Then                Position = 0            End If            If Position = VisibleFrames.Count Then                VisibleFrames.Remove SourceFrame.Name                VisibleFrames.Add SourceFrame.Name, SourceFrame            Else                VisibleFrames.Remove SourceFrame.Name                For x = 0 To VisibleFrames.Count - 1                    If x = Position - 1 Then                        tmpDict.Add SourceFrame.Name, SourceFrame                    End If                    tmpDict.Add VisibleFrames.Items(x).Name, VisibleFrames.Items(x)                Next x                Set VisibleFrames = tmpDict            End If        End If    End Sub    Private Sub ArrangeFrames()        Dim vItem As Variant        Dim lTopRow As Long        If Not VisibleFrames Is Nothing Then            If VisibleFrames.Count > 0 Then                lTopRow = pStartPosition                For Each vItem In VisibleFrames.Items                    vItem.Top = lTopRow                    lTopRow = lTopRow + vItem.Height + pSpacer                Next vItem            End If        End If    End Sub    Private Sub SortDictByArray(vSortArray As Variant, TargetDict As Dictionary)        Dim tmpDict As Dictionary        Dim vItem As Variant        Dim x As Long        If Not TargetDict Is Nothing Then            If UBound(vSortArray) = TargetDict.Count - 1 Then                Set tmpDict = New Dictionary                For x = LBound(vSortArray) To UBound(vSortArray)                    tmpDict.Add vSortArray(x, 1), TargetDict.Item(vSortArray(x, 1))                Next x                Set TargetDict = tmpDict            End If        End If    End Sub    Private Function FrameDictToArray(SourceDict As Dictionary) As Variant        Dim tmpDict As Dictionary        Dim x As Long        Dim tmpArr As Variant        Dim itm As Variant        If Not SourceDict Is Nothing Then            If SourceDict.Count > 0 Then                Set tmpDict = New Dictionary                ReDim tmpArr(0 To SourceDict.Count - 1, 0 To 1)                For Each itm In SourceDict.Items                    tmpArr(x, 0) = itm.Top                    tmpArr(x, 1) = itm.Name                    x = x + 1                Next itm                FrameDictToArray = tmpArr            End If        End If    End Function    Private Sub Sort2DArray(vArray As Variant, _        Optional ByVal lLowStart As Long = -1, _        Optional ByVal lHighStart As Long = -1)        Dim vPivot As Variant        Dim lLow As Long        Dim lHigh As Long        lLowStart = IIf(lLowStart = -1, LBound(vArray), lLowStart)        lHighStart = IIf(lHighStart = -1, UBound(vArray), lHighStart)        lLow = lLowStart        lHigh = lHighStart        vPivot = vArray((lLowStart + lHighStart) \ 2, 0)        While lLow <= lHigh            While (vArray(lLow, 0) < vPivot And lLow < lHighStart)                lLow = lLow + 1            Wend            While (vPivot < vArray(lHigh, 0) And lHigh > lLowStart)                lHigh = lHigh - 1            Wend            If (lLow <= lHigh) Then                Swap vArray, lLow, lHigh                lLow = lLow + 1                lHigh = lHigh - 1            End If        Wend        If (lLowStart < lHigh) Then            Sort2DArray vArray, lLowStart, lHigh        End If        If (lLow < lHighStart) Then            Sort2DArray vArray, lLow, lHighStart        End If    End Sub    Private Sub Swap(vArray As Variant, lItem1 As Long, lItem2 As Long)        Dim vTemp0 As Variant        Dim vTemp1 As Variant        vTemp0 = vArray(lItem1, 0)        vTemp1 = vArray(lItem1, 1)        vArray(lItem1, 0) = vArray(lItem2, 0)        vArray(lItem1, 1) = vArray(lItem2, 1)        vArray(lItem2, 0) = vTemp0        vArray(lItem2, 1) = vTemp1    End Sub

In order for the View to work with the interface, we will modify it as follows:(old code commented out)

    'Private FrameSorter As CFrameSorter    Private frameSorter As IFrameSorter    Private Sub UserForm_Initialize()        'Dim vItem As Variant        'Set FrameSorter = New CFrameSorter        'FrameSorter.Initialise Me        'Populate the combobox.        'For Each vItem In frameSorter.FrameDict.Items        '    Me.cmbFrames.AddItem vItem.Name        'Next vItem    End Sub    Public Sub ApplyFrameSorter(sorter As IFrameSorter)        Set frameSorter = sorter        frameSorter.Initialise Me        'Populate the combobox.        Dim vItem As Variant        For Each vItem In frameSorter.FrameDict.Items            Me.cmbFrames.AddItem vItem.Name        Next vItem    End Sub

And the FrameSorterTester module as follows:

    Sub TestCFrameSorter()        Dim frameSorter As IFrameSorter '<=== declare the interface        Set frameSorter = New CFrameSorter '<== create the implementing object        Dim testView As TestFrameSorterView        Set testView = New TestFrameSorterView        Load testView        testView.ApplyFrameSorter frameSorter        testView.Show    End Sub

Initiating macro TestCFrameSorter will run your code and UI just as it did before.

Although functionally equivalent, an important change has just occurred. The View no longer creates CFrameSorter. All that the View knows is that there is now a set of methods (the IFrameSorter interface) that it has access to. Now the relationship can be described as: (View to IFrameSorter): "I don't know who you are, but you are more than an interface someone gave me. You...complete me"

Now, it is time to get rid of "Initialise Me" because is passes a UI element (itself) as the parameter. So, the task becomes: how to replace the functionality ofInitialise without passing a reference to theView in theIFrameSorter interface methods.

TheInitialise subroutine basically looks at all theFrame controls on theView and loads its Dictionaries.CFrameSorter does not need theUserForm to do this - it only needs a collection ofFrame objects. So, let theView provide a collection ofFrame objects by adding a public property (read-only)Frames.

    Public Property Get Frames() As Collection        Dim myFrames As Collection        Set myFrames = New Collection        Dim ctrl As Control        For Each ctrl In Me.Controls            If TypeName(ctrl) = "Frame" Then                Select Case TypeName(ctrl.Parent)                    Case TypeName(Me)                        myFrames.Add ctrl                    Case "Frame"                        'Do nothing yet.                End Select            End If        Next ctrl        Set Frames = myFrames    End Property

And replace/comment outInitialise on theIFrameSorter interface with a new method - "LoadDictionaries":

    'Remove Initialise from the interface and add LoadDictionaries    'Public Sub Initialise(SourceForm As Object)    'End Sub    Public Sub LoadDictionaries(vFrames As Collection)    End Sub

RemovingInitialise from theIFrameSorter means that it can no longer be called from theView. MethodApplyFrameSorter is the current user ofInitialise.

In addition to setting theIFrameSorter variable,ApplyFrameSorter also loads theComboBox items. So, a better name might have been "ApplyFrameSorterAndLoadComboBoxItems". But, that 'better' name betrays the fact that the method is doing two things. TheSingle Responsibility Principle (SRP) encourages us to always write methods that 'do one thing' - and the 'one thing' should be identified by the method's name. So, in the spirit of SRP...Let's add a public PropertyFrameSorterInterface to theView in order to set/get theIFrameSorter interface - one thing. And load theComboBox (the second 'thing') some other way (Note: if we load the ComboBox as part of setting the propertyFrameSorterInterface, it would be considered an unadvertisedside-effect of calling the property - always a good idea to avoid this).

Loading the ComboBox items: TheComboBox can be loaded by the theView. There is no need to use theIFrameSorter interface to help do this. From the moment it is created, theView knows everything it needs (names of all the 'Frame' controls) in order to load theComboBox. So, the code that loads the ComboBox items can be movedback intoUserForm_Initialize.

So now, propertyFrameSorterInterface and subroutineUserForm_Initialize are each doing one thing related to their names, and together, have replaced the functionality lost by removingInitialise from theIFrameSorter interface. TheView code now looks like this:

    'TestFrameSorterView (UserForm) after removing "Initialise" from    ' the IFrameSorter interface and adding property FrameSorterInterface    Private Sub UserForm_Initialize()        'Populate the combobox.        Dim vItem As Variant        For Each vItem In Frames 'frameSorter.FrameDict.Items            Me.cmbFrames.AddItem vItem.Name        Next vItem    End Sub    Public Property Set FrameSorterInterface(sorter As IFrameSorter)        Set frameSorter = sorter    End Property    Public Property Get FrameSorterInterface() As IFrameSorter        Set FrameSorterInterface = frameSorter    End Property

Now let the FrameSorterTester be responsible for managing the initialization transactions between theCFrameSorter and theTestFrameSorterView. The macro now looks like this:

    Sub TestCFrameSorter()        Dim frameSorter As IFrameSorter        Set frameSorter = New CFrameSorter        Dim testView As TestFrameSorterView        Set testView = New TestFrameSorterView        Load testView        'Provide the View with the IFrameSorterInterface        Set testView.FrameSorterInterface = frameSorter        'Retrieve the Frame objects from the view and provide        'them to CFrameSorter so that it can load its dictionaries        Dim vFrames As Collection        Set vFrames = testView.Frames               frameSorter.LoadDictionaries vFrames        testView.Show    End Sub

Again, after all that, from a functional perspective, nothing has changed. However, any awareness of the CFrameSorter class has been extracted from the View. It only knows that it can call the IFrameSorter interface and expect the right behavior. Further, CFrameSorter no longer knows about the TestFrameSorterView - it is handed a collection of Frame controls 'from somewhere' and initializesitself. So now (View to IFrameSorter): "I don't know who you are, you are only an interface someone gave me. So, don't call me, I'll call you if (and only if) I want something". The CFrameSorter now operates in a vacuum: "I don't know where these Frame control references are coming from, but I'll do what I'm asked to do".

There is still more that can be done. The IFrameSorter interface accepts Frame control references in the method signatures. This means, that if you ever want any object to implement the IFrameSorter interface, it needs to be connected to a UI that will provide actual controls. This implies that there is no opportunity to test CFrameSorter without using an actual UI. A better version of the IFrameSorter interface eliminates UI control references.

Removing the UI controls from the interface makes IFrameSorter independent of UI elements. Writing test code without an actual UI is now possible - and preferred. So, how to move the Frames without passing aFrame control reference?...again - an interface, but this interface is on theView. Let's call this new interfaceIFrameSorterView.

So, theIFrameSorter will look something like:

    Public Sub ShowFrame(frameName As String, IFrameSorterView view)    End Sub    Public Sub HideFrame(frameName As String, IFrameSorterView view)    End Sub    Public Sub MoveUp(frameName As String, IFrameSorterView view, Optional Position As Long = 1)    End Sub    Public Sub MoveDown(frameName As String, IFrameSorterView view, Optional Position As Long = 1)    End Sub    Public Sub Move(frameName As String, IFrameSorterView view, Position As Long)    End Sub    Public Sub LoadDictionaries(frameNames As Collection)    End Sub

AndIFrameSorterView can be something like:

    Public Sub ModifyFramePosition(frameName As String, topValue As Long)    End Sub    Public Sub ModifyFrameVisibility(frameName As String, isVisible As Boolean)    End Sub

There are a lot of details to sort out to implement these two interfaces. But the goal is to extract UI and UI controls awareness fromCFrameSorter.

Regarding theCFrameSorter code, there are a couple of Dictionaries that are storing position and visibility information. This replicates what is already stored and available from theView. So, there is probably an opportunity to eliminate the Dictionaries fromCFrameSorter if theIFrameSorterView interface also includes some properties like:

    Public Property Get Top(frameName As String) As Long    End Property     Public Property Get Height(frameName As String) As Long    End Property    Public Property Get IsVisible(frameName As String) As Boolean    End Property

Or, collect them all at once...and let IFrameSorterView act as your dictionaries

   'Dictionary of Frame names to Top position values    Public Property Get FrameNamesToTop() As Dictionary     End Property     'Dictionary of Frame names to Visible values    Public Property Get FrameNamesToIsVisible() As Dictionary     End Property     'Dictionary of Frame names to Height values     Public Property Get FrameNamesToHeight() As Dictionary     End Property

Hope this was helpful. Good luck!

I am certain that you will findthis useful for your task.

answeredFeb 29, 2020 at 18:16
BZngr's user avatar
\$\endgroup\$
4
  • \$\begingroup\$Thanks for your feedback. On first read it looks like you're explaining what I've been trying to get my head around for a while now. I'll have a better read through when I haven't got half a bottle of Jack inside me & get back to you. :)\$\endgroup\$CommentedFeb 29, 2020 at 22:25
  • \$\begingroup\$Trying to work through this and have understood up to the point where you state "Initialise is gone from the interface, but the ComboBox was being loaded using the CFrameSorter. Let the View do this - it knows what Frames it has. Now ApplyFrameSorter can become a Property" - the code block below this is adding values to the combobox, although earlier in the answer you'd commented this out and moved it to theApplyFrameSorter procedure. Should it still be commented out, or be in theApplyFrameSorter? Feels like I was understanding it perfectly until I reached that point. :)\$\endgroup\$CommentedMar 2, 2020 at 10:58
  • \$\begingroup\$I've expanded the comments/explanations in that area. Hopefully it becomes more clear. And, to your specific question - I moved combo box loading out ofUserForm_Initialize and then backin toUserForm_Initialize as part of the step-by-step refactoring process that kept the code functioning after each set of modifications.\$\endgroup\$CommentedMar 2, 2020 at 15:40
  • \$\begingroup\$Have been working through your examples. It's slowly starting to click although I've still got a way to go. Again, thankyou for your feedback. It's definitely helped in taking my code to the next level.\$\endgroup\$CommentedMar 12, 2020 at 9:22

You mustlog in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.