VBA Settings Dialog using MVP - do I need a model? - vba

I've been reading up on many examples of MVP (Model-View-Presenter) and their variations (Passive view, Supervising view) to try and make my solutions more robust (and reusable) in VBA (using Excel as the host in this instance). The problem I've found is finding good, simple examples in VBA that are not complete overkill for the (hopefully) simple examples I need.
I'm attempting to create a "settings" dialogue that stores certain configuration in a worksheet (this is my "repository").
Here's my main procedure, triggered by the user:
Private Sub ShowImportSelector()
Dim importPresenter As DataImportPresenter
Set importPresenter = New DataImportPresenter
importPresenter.LoadConfig
If importPresenter.Show = -1 Then Exit Sub
importPresenter.SaveConfig
' begin processing...
If (CStr([Settings.SelectedVersion].Value2) = "QQ") Then
' ...
End If
End Sub
Here is my "presenter" (here I use range names for the source, and config destination):
Option Explicit
Private m_importForm As FImport
Private Sub Class_Initialize()
Set m_importForm = New FImport
End Sub
Public Sub LoadConfig()
m_importForm.SetAvailableVersions "tblVERSION"
m_importForm.SetAvailableSalesOrgs "tblSALESORG"
m_importForm.SetAvailableCategories "tblCATEGORY"
m_importForm.ToolName = "Forecast"
End Sub
Public Sub SaveConfig()
[Settings.SelectedVersion].Value2 = m_importForm.SelectedVersion
[Settings.SelectedSalesOrg].Value2 = m_importForm.SelectedSalesOrg
[Settings.SelectedCategory].Value2 = m_importForm.SelectedCategory
End Sub
Public Function Show() As Integer
m_importForm.Show vbModal
Show = m_importForm.Result
End Function
And now the "View" (a VBA Form):
Option Explicit
Private m_selectedVersion As String
Private m_selectedSalesOrg As String
Private m_selectedCategory As String
Private m_toolName As String
Private m_dialogueResult As Long
Public Property Get ToolName() As String
ToolName = m_toolName
End Property
Public Property Let ToolName(ByVal value As String)
m_toolName = value
ToolNameLabel.Caption = value
End Property
Public Property Get Result() As Long
Result = m_dialogueResult
End Property
Public Property Get SelectedVersion() As String
SelectedVersion = m_selectedVersion
End Property
Public Property Get SelectedSalesOrg() As String
SelectedSalesOrg = m_selectedSalesOrg
End Property
Public Property Get SelectedCategory() As String
SelectedCategory = m_selectedCategory
End Property
Public Sub SetAvailableVersions(ByVal value As String)
VersionSelector.RowSource = value
End Sub
Public Sub SetAvailableSalesOrgs(ByVal value As String)
SalesOrgSelector.RowSource = value
End Sub
Public Sub SetAvailableCategories(ByVal value As String)
CategorySelector.RowSource = value
End Sub
Private Sub SaveSelections()
m_selectedVersion = VersionSelector.value
m_selectedSalesOrg = SalesOrgSelector.value
m_selectedCategory = CategorySelector.value
End Sub
Private Sub CloseButton_Click()
m_dialogueResult = -1
Me.Hide
End Sub
Private Sub ImportButton_Click()
SaveSelections
m_dialogueResult = 0
Me.Hide
End Sub
At this point, I have become confused with the possible directions I could go in terms of adding a model to the above - question is: is this even needed for this simple example?

MVP architecture makes cleaner code, but cleaner code isn't the primary purpose of MVP; achieving loose coupling, higher cohesion, and testability is.
If loosely-coupled components and unit-testable logic isn't a requirement, then full-blown MVP is indeed overkill, and having the "model" exposed as properties on the "view" is definitely good enough, as it already helps making your "presenter" not need to care about form controls. You're treating the form as the object it's begging to be, and pragmatically speaking this could very well be all you need. I'd make the Show method return an explicit Boolean though, since it's implicitly used as such.
On the other hand, if you are shooting for decoupling and testability, then extracting the model from the view would only be step one: then you need to decouple the presenter from the worksheet, and maybe introduce some ISettingsAdapter interface that abstracts it away, such that if/when the configuration needs to go to a database or some .config file, your presenter code doesn't need to change in any way... but this requires designing the interfaces without having any particular specific implementation in mind, i.e. something that works without changes regardless of whether the data is on a worksheet, in some flat file, or in some database table.
MVP demands a paradigm shift: MVP isn't procedural programming anymore, it's OOP. Whether OOP is overkill for your needs depends on how much coupling you're willing to live with, and how frail this coupling is making your code in the face of future changes. Often, abstraction is enough: using named ranges instead of hard-coded range addresses is one way of improving the abstraction level; hiding the worksheet behind an adapter interface implemented by a worksheet proxy class (whatever you do, never make a worksheet module implement an interface: it will crash) is another - depends where your threshold for "overkill" is, but if you do achieve full decoupling and write the unit tests, nobody can blame you for going overboard: you're just following the industry best-practices that every programmer strives for, improving your skills, and making it much easier to later take that code and rewrite it in .NET, be it VB or C#. I doubt anyone would argue that full-blown MVP is overkill in .NET/WinForms.

Related

VBA - Passing variable between modules

I'm new to VB6, and trying to write some macro in use for CorelDraw.
I have a variable that need to be passed from Class module to Standard module, in my Class Module "SaveOptClass" I have a public variable called IsSaved and it's set on the class module:
Public IsSaved As Boolean
Public Sub SaveFile()
If <some triggers> Then
IsSaved = True
End If
In Standard module:
Sub DoSave()
Dim SaveClass As SaveOptClass
Set SaveClass = New SaveOptClass
If SaveClass.IsSaved = True Then
ActiveDocument.Save
Else
Form1.Show
End If
End Sub
Basically I'm trying to pass "IsSaved" boolean value from class module to standard. (If IsSaved is true, save the document or else display a form.)
I have tested that the boolean is True when I executed the code, but I can't get the state to pass to the other module.
Is there something I miss here? Thanks in advance.
As already answered by #shahkalpesh the problem is that you're not using a meaningful instance of SaveOptClass.
In my opinion the best way to design this kind of dependency is by mean of a parameter in the routine is using it and avoid as much as possible the use of global variables.
In your case this brings to this rewriting:
' in someOtherModule
Public Sub DoSave(saveOptObj as SaveOptClass)
If saveOptObj.IsSaved Then ' = True is unnecessary
ActiveDocument.Save
Else
Form1.Show
End If
End Sub
The client code could be:
private saveOptObj as SaveOptClass
Public Sub SaveFile()
If <some triggers> Then
saveOptObj.IsSaved = True
End If
' ....
someOtherModule.DoSave(saveOptObj)
' ...
Consider also, at this point, a renaming of DoSave, given that the actions taken suggest different semantics. In similar cases is preferable moving the If Else logic in the caller. Anyway, if you prefer to group actions with different semantics in the same routine you'd better use namings like DoSaveOr<SomethingElse>.

I seem to be getting pointer behavior instead of copying the value of an object?

This is my first post, and I have been looking around, but cannot find an answer to my problem, as I'm not even sure the proper terminology for it, and I'm not entirely sure how to explain it, but I'll try through a tiny example that is essentially a simplified version of my actual code:
Public Class myCustomClass
Public myValue as String
Public Sub New()
With Me
.myValue = String.Empty
End With
End Sub
End Class
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles Me.Load
Dim myObject_A As New myCustomClass
myObject_A.myValue = "Sample String"
Dim myObject_B As New myCustomClass
myObject_B = myObject_A
myObject_B.myValue = "New Sample String"
End Sub
The problem I am having is, when I change the value of myObject_B.myValue to "New Sample String", the same change is made to myObject_A.myValue, as though instead of copying the value from myObject_A to myObject_B, myObject_B is acting like a pointer to myObject_A.
What I want is to make myObject_B the same value as myObject_A, but not link the two together. I have only started noticing this behavior since upgrading to Visual Studio Express 2012 for Windows Desktop. In previous versions I don't think this is how my variables behaved.
I suspect I'm doing something wrong, and I also suspect something was changed in VS 2012 for Desktop. I'm more than willing to share my actual code if needed, I just didn't want to paste a bunch of stuff if the above was enough to figure out where I'm screwing up.
Sincerely, Brian
Update:
Thank you for the Cloning link, and coding example. It helps me understand what is being talked about, and gives me something to read up on further when I have more time. This is entirely new for me and I appreciate learning about it. It brings me to my next question, however, which is that I'm actually making lists of myCustomClass. Is it possible to implement cloning on an entire list, or will I need to create a list of New myCustomClass objects, then iterate through each list item and use the clone method on each one?
Dim myList as List(Of myCustomClass)
I'm not sure how to implement cloning for a list of myCustomClass.
What I want is to make myObject_B the same value as myObject_A, but not link the two together.
The simplest way to achieve that would be to:
Dim myObject_A As New myCustomClass
Dim myObject_B As New myCustomClass
myObject_A.myValue = "Sample String"
myObject_B.myValue = myObject_A.myValue
This will not link them together but place myValue from myObject_A in to myObject_B.
It is a quick thing to do, however, if you have complex objects, I would suggest implementing Clone() method in your class.
More information on Cloning.
Update:
I believe your updated question is answered here
In this case myClass is a Class instance and has pointer like semantics. Assigning between two instances will just cause them to refer to the same object it won't create independent copies. If you want to have value behavior then make myClass a Structure instead of a Class
Add a Clone method in your myCustomClass
Public Class myCustomClass
Public myValue as String
Public Sub New()
With Me
.myValue = String.Empty
End With
End Sub
Public Function Clone() As myCustomClass
Return DirectCast(Me.MemberwiseClone, myCustomClass)
End Function
End Class
And assign your myObject_B like this ...
myObject_B = myObject_A.Clone()
Reference: Object.MemberwiseClone method

Error capturing onreadystatechange of MSXML2.DOMDocument in VBA

I am getting an error trying to arrange asynchronous loading and parsing of an XML document in VBA using a wrapper class.
Following the ideas described in this msdn article and this tutorial which have worked perfectly for asynchronous handling of MSXML2.XMLHTTP40.send method I attempted to do a similar thing for DOMDocument.loadXML.
Here is the code from the wrapper class DOMMonitor
Private domDoc As MSXML2.DOMDocument
Public Event onXmlLoadComplete(d As MSXML2.DOMDocument)
Public Sub loadXML(XmlFilePath As String)
Set domDoc = CreateObject("MSXML2.DOMDocument")
domDoc.async = True
domDoc.onreadystatechange = Me ' error occurs here
domDoc.Load XmlFilePath
End Sub
Public Sub onLoadComplete()
If domDoc.readyState = "4" Then
RaiseEvent onXmlLoadComplete(domDoc)
End If
End Sub
I have made onLoadComplete the default method by setting VB_UserMemId = 0, so it is supposed to be invoked when domDoc fires onreadystatechange .
However when I invoke loadXML
Dim dm As DomMonitor
Set dm = New DomMonitor
dm.loadXML txtXMLData
i get the following runtime error in this line:
domDoc.onreadystatechange = Me
This object cannot sink the 'onreadystatechange' event. An error occurred marshalling the object's IDispatch interface
What am I doing wrong and is there a good workaround here?
Thanks in advance.
P.S. The reason I am republishing the event is that I do not necessarily want use the default method of the final subscriber for this purpose. However, as things stand now I do not even get to that stage.
The way I read that msdn article is that to assign a wrapper class to the readystatechange, the object has to be either an IXMLHTTPRequest or an IServerXMLHTTPRequest object (bullet 3). Since your object is a DOMDocument, readystatechange doesn't accept an object.
However, you can instantiate a DOMDocument WithEvents (bullet 2), making the other way redundant, I guess. I don't have an xml file large enough to test, but I think this should work. I assume that if the class loses scope, all bets are off, so I made it a global variable.
In a standard module
Public clsDOMMonitor As CDOMMonitor
Sub test()
Set clsDOMMonitor = New CDOMMonitor
clsDOMMonitor.loadXML "C:\Users\dkusleika\Downloads\wurfl-2.3.xml"
End Sub
In CDOMMonitor class
Private WithEvents mDoc As MSXML2.DOMDocument
Private Sub mDoc_onreadystatechange()
If mDoc.readyState = 4 Then
MsgBox "second"
End If
End Sub
Public Sub loadXML(XmlFilePath As String)
Set mDoc = New MSXML2.DOMDocument
mDoc.async = True
mDoc.Load XmlFilePath
MsgBox "first"
End Sub
I assume that setting async to True is all that is needed for this to work properly. My 100k xml file is probably done so fast that that the event never gives up control. But if you had a sufficiently large xml file, I think you would get "first" before "second".
Change the class' Instancing property from Private to PublicNotCreatable when late binding, whilst also applying the tweak which you have mentioned.
Use the above example when early binding (as in your case).

Passing two similar forms as the same type and still being able to access its objects

I am trying to implement another form into already complete routine. Basically all the code is there, all I need to do is manipulate the data in a different manner.
I have a routine that looks like this for instance.
This is a paraphrase example:
Private Sub getReportValues(ByRef fr As frmCustomReport, ByRef ReportInfo As ReportValues)
ReportInfo.eHeaderColor = Microsoft.Win32.Registry.CurrentUser.CreateSubKey("Software\FE Jupiter\MSSMonitor").GetValue("Report Equipment Header Color", "DCDCDC") 'Gainsboro
ReportInfo.mHeaderColor = Microsoft.Win32.Registry.CurrentUser.CreateSubKey("Software\FE Jupiter\MSSMonitor").GetValue("Report Monitor Header Color", "FFF8DC") 'Cornsilk
fr.btnEquipColor.PickedColor = System.Drawing.ColorTranslator.FromHtml("#" & Microsoft.VisualBasic.Conversion.Hex("&HFF" & ReportInfo.eHeaderColor))
The problem lays with the (fr as frmCustomReport) I want to make it a system.windows.forms.form but then I would lose the ability to use its objects. I should also mention that fr is a modal dialog and that I don't want a really hacky controlcollection work around for this. Does anyone have a good direction on this?
Note Also!!! The controls I want to access on both forms are almost identical. The only diffrence is layout and some added functionality.
Without a little more information,it is a little hard to give a concrete example. This will work depending on the amount of interaction you need to do. Create a subroutine that accepts the Base Class as an argument, take a look at the Name value and base your conditional logic off of that using CType to cast the Form to the proper type.
Private Sub clickOtherFormsButton(Value As Form)
If Value.Name = "Form3" Then
Dim formObject As Form3 = CType(Value, Form3)
formObject.Button1.PerformClick()
ElseIf Value.Name = "Form2" Then
Dim formObject As Form2 = CType(Value, Form2)
formObject.Button1.PerformClick()
End If
End Sub

Restrict type in a Collection inside a class module

I have a collection inside a class module. I'd like to restrict the object type that is "addable" to this collection, i.e. collection should only ever accept objects of one given type and nothing else.
Is there any way to enforce the type of objects added to a collection?
From what I can tell, there is no built-in way to do this. Is the solution then to make this collection private, and build wrapper functions for the methods usually accessible for Collections, i.e. Add, Remove, Item, and Count?
I kinda hate having to write 3 wrapper functions that add no functionality, just to be able to add some type enforcement to the Add method. But if that's the only way, then that's the only way.
There is no way to avoid wrapper functions. That's just inherent in the "specialization through containment/delegation" model that VBA uses.
You can build a "custom collection class", though. You can even make it iterable with For...Each, but that requires leaving the VBA IDE and editing source files directly.
First, see the "Creating Your Own Collection Classes" section of the old Visual Basic 6.0 Programmer's Guide:
http://msdn.microsoft.com/en-us/library/aa262340(v=VS.60).aspx
There is also an answer here on stackoverflow that describes the same thing:
vb6 equivalent to list<someclass>
However, those are written for VB6, not VBA. In VBA you can't do the "procedure attributes" part in the IDE. You have to export the class module as text and add it in with a text editor. Dick Kusleika's website Daily Dose of Excel (Dick is a regular stackoverflow contributer as you probably know) has a post from Rob van Gelder showing how to do this:
http://www.dailydoseofexcel.com/archives/2010/07/04/custom-collection-class/
In your case, going to all that trouble - each "custom collection" class needs its own module - might not be worth it. (If you only have one use for this and it is buried in another class, you might find that you don't want to expose all of the functionality of Collection anyway.)
This is what I did. I liked Rob van Gelder's example, as pointed to by #jtolle, but why should I be content with making a "custom collection class" that will only accept one specific object type (e.g. People), forever? As #jtolle points out, this is super annoying.
Instead, I generalized the idea and made a new class called UniformCollection that can contain any data type -- as long as all items are of the same type in any given instance of UniformCollection.
I added a private Variant that is a placeholder for the data type that a given instance of UniformCollection can contain.
Private mvarPrototype As Variant
After making an instance of UniformCollection and before using it, it must be initialized by specifying which data type it will contain.
Public Sub Initialize(Prototype As Variant)
If VarType(Prototype) = vbEmpty Or VarType(Prototype) = vbNull Then
Err.Raise Number:=ERR__CANT_INITIALIZE, _
Source:=TypeName(Me), _
Description:=ErrorDescription(ERR__CANT_INITIALIZE) & _
TypeName(Prototype)
End If
' Clear anything already in collection.
Set mUniformCollection = New Collection
If VarType(Prototype) = vbObject Or VarType(Prototype) = vbDataObject Then
' It's an object. Need Set.
Set mvarPrototype = Prototype
Else
' It's not an object.
mvarPrototype = Prototype
End If
' Collection will now accept only items of same type as Prototype.
End Sub
The Add method will then only accept new items that are of the same type as Prototype (be it an object or a primitive variable... haven't tested with UDTs yet).
Public Sub Add(NewItem As Variant)
If VarType(mvarPrototype) = vbEmpty Then
Err.Raise Number:=ERR__NOT_INITIALIZED, _
Source:=TypeName(Me), _
Description:=ErrorDescription(ERR__NOT_INITIALIZED)
ElseIf Not TypeName(NewItem) = TypeName(mvarPrototype) Then
Err.Raise Number:=ERR__INVALID_TYPE, _
Source:=TypeName(Me), _
Description:=ErrorDescription(ERR__INVALID_TYPE) & _
TypeName(mvarPrototype) & "."
Else
' Object is of correct type. Accept it.
' Do nothing.
End If
mUniformCollection.Add NewItem
End Sub
The rest is pretty much the same as in the example (plus some error handling). Too bad RvG didn't go the whole way! Even more too bad that Microsoft didn't include this kind of thing as a built-in feature...
I did almost the same code of Jean-François Corbett, but I adapted because for some reason wasn't working.
Option Explicit
Public pParametro As String
Private pColecao As New Collection
Public Sub Inicializar(ByVal parametro As String)
pParametro = parametro
End Sub
Public Sub Add(NewItem As Object)
If TypeName(NewItem) <> pParametro Then
MsgBox "Classe do objeto não é compatível à coleção"
Else
pColecao.Add NewItem
End If
End Sub
Public Property Get Count() As Long
Count = pColecao.Count
End Property
Public Property Get Item(NameOrNumber As Variant) As Variant
Set Item = pColecao(NameOrNumber)
End Property
Sub Remove(NameOrNumber As Variant)
pColecao.Remove NameOrNumber
End Sub
Then, when i want to create an instance from CCollection I make like the code bellow:
Set pFornecedores = New CCollection
pFornecedores.Inicializar ("CEmpresa")
Where CEmpresa is the class type from the object I want
Yes. The solution is to make your collection private and then make public wrapper functions to add, remove, getitem and count etc.
It may seem like hassle to write the additional code but it is a more robust solution to encapsulate the collection like this.