Tags

, , , ,

The first part of this series introduced the architecture I am using to implement MVP in VB6 and can be found here. In this second part I am going to dive deeper into the code but first I have to cover one of the main problems I had: file management.

Practicalities

Since my last post my projects have moved on and increased in complexity and size as is expected. An interesting point is that the number of stories increased but the code seems to have decreased. Of course this is illusory. What is happening is that all my UI code is scattered across dozens of different classes instead of concentrated behind the form or control. What is also true is that the VB6 IDE is totally inappropriate for the increasing number of files. Its static division of files by type or the prosaic file list in the project pane do not scale at all. My current solution is to create a different project and move there all the interfaces and argument classes (classes used in the argument list of the interfaces). I start by creating all the MVP files for a user story in the main project, implement all the functionality and move the non-functional classes when I am finished with the story. I leave the presenters, the views and models in the main project as they contain the “meat” of my UI logic and help me to avoid duplicating functionality. I also tend to share the model between MVPs but I am still not sure if this is the best approach as it has lead to unwanted side effects. I may change this at a later stage. And now, we dive into the code.

The Presenter class

This is the most important class or entity in this design. The application behaviour is partly coded here, partly in the model. I decide what goes where by articulating the user requirement in the presenter and implementing the functionality in the model. A typical presenter can be as simple as:

'-------------------------------------------------------
' Story: When the user selects a data range for the
' first time create a new Run. All subsequent selections
' replace the initial data. Display feedback to the
' user after the data is loaded and when there is an
' error loading the data. In this latest case also
' scroll to the ofending row.
'-------------------------------------------------------
Option Explicit On   

Implements DataSelectedViewEvents
Implements DtaSelectedModelEvents
'-------------------------------------------------------
' Views and Models
Private m_View As DataSelectedViewInter
Private m_Model As DataSelectedModelInter                    

Public Sub Init(ByVal view As DataSelectedViewInter, ByVal model As DataSelectedModelInter)
    m_View = view
    m_Model = model
    CIListener(view).SetListener(Me)
    CIListener(model).SetListener(Me)
End Sub                    

Sub DataSelectedViewEvents_DataSelected(ByRef pWb As Workbook, ByRef pData() As Variant)             

    If m_Model.RunLoaded Then
        m_Model.ReplaceRunData(pWb, pData)
    Else
        m_Model.CreateNewRun(pWb, pData)
    End If
End Sub             

Sub DataSelectedModelEvents_DataLoaded(ByRef pInfo As DataInformation)             

    m_View.ShowDataInfo(pInfo)
End Sub                    

Sub DataSelectedModelEvents_InvalidData(ByVal pErrorMessage As String)             

    m_View.ShowError(pErrorMessage)
End Sub                    

Private Sub DataSelectedModelEvents_ScrollToAddress(ByVal pAddress As String)             

    m_View.ScrollToAddress(pAddress)
End Sub

The user story is “When the user selects a data range” in a VB6 addin for Excel. The class is divided in three parts: the infrastructure code, the view event handling and the model event handling. As discussed before the view and the model communicate with the presenter through a custom event handling mechanism based on custom interfaces.
The infrastructure is composed of this code:

Implements DataSelectedViewEvents
Implements DataSelectedModelEvents
'-------------------------------------------------------
' Views and Models
Private m_View As DataSelectedViewInter
Private m_Model As DataSelectedModelInter            

Public Sub Init(ByVal view As DataSelectedViewInter, ByVal model As DataSelectedModelInter)            

    m_View = view
    m_Model = model            

    CIListener(view).SetListener(Me)
    CIListener(model).SetListener(Me)
End Sub            

Sub DataSelectedViewEvents_DataSelected(ByRef pWb As Workbook, ByRef Target As Excel.Range)            

    If m_Model.RunLoaded Then
        m_Model.ReplaceRunData(pWb, pData)
    Else
        m_Model.CreateNewRun(pWb, pData)
    End If
End Sub

The two Implements represent the events that can be raised by the view and the model, the view and model themselves are present as class members on which the presenter can act and the Init method is called during the initialisation of the whole application when all the wiring is put in place.
There is only one view event:

Private Sub m_App_SheetSelectionChange(ByVal Sh As Object, ByVal Target As Excel.Range)           

    m_DataSelectedEvents.DataSelected(m_Workbook, Target)
End Sub

This event is raised by the view when the user finishes selecting a range on the spreadsheet. The view code is one line long and hard to get wrong:

Private Sub m_App_SheetSelectionChange(ByVal Sh As Object, ByVal Target As Excel.Range)           

    m_DataSelectedEvents.DataSelected(m_Workbook, Target)
End Sub

My intent is to capture the selection and load it into my backend business object (a Run). On top of that, the first selection in a new spreadsheet causes the creation of a new Run, all subsequent selections replace the previously loaded data. As you can see the behaviour of the application is encoded in the presenter whereas the mechanics of loading data and creating new objects are delegated to the model.
There are three model events that flesh out the reaction to the data load. Each event passes the information produced by the model to the view for display underlining the fact that the model and the view are completely unaware of each other. There is pragmatism though, the second and third events transmit strings but the first one passes along an object. This object contains several data parameters and is fed to a custom control in the view avoiding long lists of arguments.

Sub DataSelectedModelEvents_DataLoaded(ByRef pInfo As DataInformation)           

    m_View.ShowDataInfo(pInfo)
End Sub           

Sub DataSelectedModelEvents_InvalidData(ByVal pErrorMessage As String)           

    m_View.ShowError(pErrorMessage)
End Sub           

Private Sub DataSelectedModelEvents_ScrollToAddress(ByVal pAddress As String)           

    m_View.ScrollToAddress(pAddress)
End Sub

The View class

The view implements the DataSelectedViewInter interface and the implementation is very simple. So simple that, ideally, we don’t need to test it beyond the obvious sanity check:

'------------------------------------------------------
' DataSelectedViewInter implementation
'------------------------------------------------------
Private Sub DataSelectedViewInter_ShowDataInfo(ByVal pInfo As DataInformation)              

    ' This assignment triggers the control redraw routine
    Set m_DataDefinitionViewer.DataSource = pInfo
End Sub              

Private Sub DataSelectedViewInter_ShowError(ByVal pMessage As String)              

    MsgBox pMessage, vbInformation
End Sub                    

Private Sub DataSelectedViewInter_ScrollToAddress(ByVal pAddress As String)              

    m_Workbook.ActiveSheet.Range(pAddress).Select()
End Sub              

Private Sub DataSelectedViewInter_DataLoadFailed()              

    MsgBox "It is not possible to load the selected data into the run." _
               "Please check the selected row.", vbInformation
End Sub

Another aspect already touched above is the events raised by the view and handled in the presenter. In this case there is only one which is quite typical.

'------------------------------------------------------
' View events caught by the Presenter
'------------------------------------------------------
Private Sub m_App_SheetSelectionChange(ByVal Sh As Object, ByVal Target As Excel.Range)              

    m_DataSelectedEvents.DataSelected(m_Workbook, Target)
End Sub

If you find that your view methods grow beyond any infrastructural code (like dealing with threading issues, for example) plus one or two lines of code that is a sign that you are handling two or more user stories in the same MVP. It is very tempting to add a just a little bit of validation here a couple of ifs there but in most cases you will be postponing the inevitable. The main reason to steer away from multi-story MVPs is the complexity already present in this pattern. Your code is scattered over three to five classes which make it difficult to review and debug and it is very easy to introduce side effects too. Constraining yourself to a single story per MVP keeps this complexity to a minimum. In my experience two stories are one too many. Very soon after coding such MVPs I end up having to refactor them into two or more MVPs with the added agro of reviewing already the implemented functionality.

The Model

The model can be the keeper of your state and, effectively, your business object or it can be tied to the UI functionality and behaviour. You do have to choose either way early on and you also have to decide if you are going to create one model per MVP or if you are going to share a single model between MVPs. So far I have been sharing the model in all the projects where I use MVP, be them VB6 or .NET, large or small. As I wrote before I am not sure if this is the right thing to do and I have had problems with the shared state but I am playing the pragmatist and solving the problems as they come along. My models are in general pretty simple farming out any process that is not directly related to the UI to backend libraries. So, so far my advice is to use shared models to reduce the number of files and inevitable code and state duplications but use the models for UI decisions only.
A typical model will expose functionality to the presenter and raise events when its state has changed or when it requires UI intervention. Below you can see part of a model where the DataSelectedModelInter interface is being implemented.

'------------------------------------------------------
' DataSelectedModelInter implementation
'------------------------------------------------------
Private Sub DataSelectedModelInter_CreateNewRun(ByVal wb As Workbook, ByRef pData() As Variant)                

    If Not CheckDataSet(wb, pData) Then
        m_DataSelectedEvents.CancelDataSelection()
        Exit Sub
    End If                

    Dim proxy As Object
    Set proxy = CreateObject("myBackendLib.NewRun")
    Set m_RunInfo = New RunInformation                

    If proxy.CreateNewRun(pData, ProblemCategoryEnum.Classification) Then
        Set m_RunInfo = proxy.GetRunfInfo
        m_DataSelectedEvents.DataLoaded(m_RunInfo)
    End If
End Sub
...

This is a very short but complete example. The model implements DataSelectedModelInter and communicates with the presenter using the class member m_DataSelectedEvents. As you can see the task of creating the business object is delegated to a backend library keeping the model simple and focused on the user interface functionality.

Advertisements