vba Class Property Values, setting default values, storing values to string - vba

another stumped newb here, trying to wrap my head around a problem.
I'm trying to use a class module, instead of public variables, with property get and set.
I want to set these values in a userform with text boxes, and update a listbox in the userform as they are entered, preferably on a text box event _afterupdate,
When the user hits the save button I'd like the profile attributes to be stored to a range.
No doubt this is going to be a laughable mess, but I've been stumped for days, and I'm coming here hat in hand. I just can't figure it out.
Class Mod Example:
Private mProfileName As String
Private mStartDate As Date
Private mEndDate As Date
Private mOngoing As Boolean
Sub Class_Initialise()
'Set default values for properties
mLastName = "Enter Last Name"
mStartDate = "Enter Date"
mEndDate = Date
mOngoing = True
End Sub
'********************************
'The relevant property procedures:
'********************************
Property Get ProfileName() As String
ProfileName = mProfileName
End Property
Property Let ProfileName(Value As String)
mProfileName = Value
End Property
Property Get EndDate() As Date
EndDate = mEndDate
End Property
Property Let EndDate (Value As Date)
mEndDate = Value
End Property
Property Get Ongoing() As Boolean
Ongoing = mOngoing
End Property
Property Let Ongoing(Value As Boolean)
If mEndDate = Date Then
mOngoing = True
End If
End Property
In the Userform I currently have:
Option Explicit
'hopefully not needed:
'Private mTextBoxUpdated As Boolean
'Private mListBoxUpdated As Boolean
'Private mEnteredText As String
'Private mIndexText As String
Private DictThisForm As Dictionary
Private ProfileData As clsProfileData
Private Sub UserForm_Initialize()
Debug.Print "UserForm Intialised"
Set DictThisForm = New Dictionary
Debug.Print "DictThisForm Created"
Set ProfileData = New clsProfileData
Call UserForm_UpdateListBox
And this laughable mess: (not even close to working)
Sub UserForm_UpdateListBox()
With lbxListBox1
.Clear
.ColumnCount = 2
.AddItem
'.List(0, 1) = "Profile Name",ProfileData.ProfileName '
'another attempt
'the below throws a Type Mismatch Error
.AddItem ProfileData.ProfileName, "Profile Name"
.AddItem ProfileData.StartDate, "Start Date"
.AddItem ProfileData.EndDate, "End Date"
.AddItem ProfileData.OnGoing, "Ongoing?"
And (with privately declared module level variables)
Private Sub tbxProfileName_AfterUpdate()
ProfileData.FirstName = tbxProfileName.Text
enter code here
Call UserForm_UpdateListBox
End Sub
At the moment I'm just trying to test the class module and see if I can get the properties values into a variable, or better, get those property values into a range on a hidden page, maybe via a dictionary, and update the list from there? Getting the property values into a listbox seems to be an unwieldy mess...
Sub testClassProfile()
Dim getPropertyAsStringVar As String
Dim NewProfile As clsProfileData
Set NewProfile = New clsProfileData
getPropertyAsStringVar = NewProfile.FirstName
Debug.Print getPropertyAsStringVar
End Sub
Current Debug.Print output is "" ie zip. No default values.
Any advice greatly appreciated.
Please let me know if I'm asking too much at once and I'll try to narrow the scope of the question just to my current issue, I thought the context might be helpful...

Not really an "answer", but it's too hard to respond to your comment with another comment!
Here is an example of how you can pass a class to a userform. Might help!
The TestModule Code
Option Explicit
Public Sub test()
'//Basic declaration of a UserForm
Dim testUF As UserForm1
Set testUF = New UserForm1
'//Now we create a test class - see the Property Get/Set for the string.
Dim testC1 As Class1
Set testC1 = New Class1
testC1.TestString = "Hello"
'//In the UserForm, I've created a property that accepts a class. Assign the
"TestClass" instance tothe UserForm Instance
testUF.Class1 = testC1
testUF.Show
End Sub
...And here is the userform. Note the property that I've added to accept a class instance.
Option Explicit
Private classInstance As Class1
Public Property Get Class1() As Variant
Set Class1 = classInstance
End Property
Public Property Let Class1(ByVal vNewValue As Variant)
Set classInstance = vNewValue
End Property
Private Sub UserForm_Click()
Me.Caption = classInstance.TestString
End Sub
And the class:
Option Explicit
Private mtest As String
Public Property Get TestString() As String
TestString = mtest
End Property
Public Property Let TestString(ByVal vNewValue As String)
mtest = vNewValue
End Property
When you run the "Test" code, you will see that instances of a class and a userform (which is actually just a "Class" as well) are created, and one instance is passed to another. It is (behind the scenes) passed "by reference", so any changes made to TestCl within the userform would be retained outside of the class, allowing it to be modified by the UserForm, and then returned back to the controlling code for use later.

The signature for the Initialize method is
Private Sub Class_Initialize()
... that's not what you have so it will not be run when an object based on the class is created. To make sure it's correct, inside your class code module select "Class" from the left-hand drop-down at the top, then select the required method from the right-hand selection.
Edit: this looks off -
Property Get Ongoing() As Boolean
Ongoing = mOngoing
End Property
Property Let Ongoing(Value As Boolean)
If mEndDate = Date Then
mOngoing = True
End If
End Property
The Ongoing property doesn't really depend on the backing field or need to be set (since its value depends only on mEndDate, so you can drop the Let and just use a Get something like this -
Property Get Ongoing() As Boolean
Ongoing = (mEndDate <= Date)
End Property

Related

How to access public class property and preserve/modify an instance of class property value in several modules

Class LoanApplicant
Private mstrPropertyName As String
Property Get Name() As String
Name = mstrPropertyName
End Property
Property Let Name(rData As String)
mstrPropertyName = rData
End Property
Sub to initialise the class and assign a value to the property
Public ApplicantName As String
Sub Initilise()
Dim Applicant1 As LoanApplicant
ApplicantName = "Steve"
Set Applicant1 = New LoanApplicant
Applicant1.Name = "Frank"
End Sub
Sub to print, first message box works as it is a public variable but second does not.
Sub CreateClass(Applicant1 As Object)
Call Initilise
MsgBox (ApplicantName)
MsgBox (Applicant1.Name)
End Sub
Create a class with a property
Then, I have two modules. let's say in the first module, I initilise the class and create an instance of a class, assigned value to the class property etc.
Question is, how do I access the same instance property value in another module? I have tested in my example that an instance of the class would be destroyed when exit a sub. the value of the class property is not preserved.
Thanks
Rational for the question is that in real world, when create an applicant class, it can have multiple applicant instance, applicant1, applicant2 etc. At various modules, the program would have to modify the same applicant's attributes(properties).
In your Initilise procedure, once you create an instance of LoanApplicant and assign the Name property a name, you need to call your CreateClass procedure and pass it your newly created object.
Here's an example for you to look at. Notice, though, I changed the name of your procedure from CreateClass to DisplayMessage, in order to reflect its true purpose.
Public ApplicantName As String
Sub Initilise()
ApplicantName = "Steve"
Dim Applicant1 As LoanApplicant
Set Applicant1 = New LoanApplicant
Applicant1.Name = "Frank"
DisplayMessage Applicant1
End Sub
Sub DisplayMessage(Applicant As Object)
MsgBox ApplicantName
MsgBox Applicant.Name
End Sub
So I got a new version of what I want.
Overall module and subs
Sub Main()
Dim Applicant1 As LoanApplicant
Set Applicant1 = New LoanApplicant
ApplicantCreate Applicant1
IncomeEntry Applicant1
ExpenseEntry Applicant1
MsgBox (Applicant1.name)
MsgBox (Applicant1.Income)
MsgBox (Applicant1.Expense)
End Sub
Sub ApplicantCreate(Applicant As Object)
Applicant.name = "Frank"
End Sub
Sub IncomeEntry(Applicant As Object)
Applicant.Income = 100000
End Sub
Sub ExpenseEntry(Applicant As Object)
Applicant.Expense = 5000
End Sub
Each sub can be placed into different module that does not change the results of what I wanted.
This is just a simplified version of the business logic it would apply in each sub. In reality, it can be much more complex. So, I got that.
Next question in line is whether it is possible to have an alternative approach? I mean, this works fine when I know there is only ONE applicant. What if I do not know how many applicants would be generated in Main? Also, do I have to create all the instant of different object in Main? If I use this approach, I would think I have to but I just like to know if there are other options. For example, if I can create dynamic object variable in any of the sub?

Add A Method To A Property Of A Class

I am confused. I am new to VBA classes. I want to add multiple methods to a property of a class, or add properties to another property. I may not have the terminology correct?
I can add one property, but I want to drill down deeper.
For instance if I make a class person:
PersonClass.Features.Hair.Texture.Color
PersonClass.Features.Hair.Texture.Style
PersonClass.Features.Hair.Length
I am not sure how to go about this.
e.g.
MyClass.MyProperty.MyMethod1
MyClass.MyProperty.MyMethod2
MyClass.MyProperty.MyMethod3
or
MyClass.MyProperty.MyMethod1.MyMethod2
Here is an example to illustrate the concepts mentioned in the comments:
Main Form
Option Explicit
Private Sub Form_Load()
Dim p As Person
Set p = New Person
p.Features.Hair = "Red"
MsgBox p.Features.Hair
End Sub
Person Class
Option Explicit
Private m_Features As Features
Private Sub Class_Initialize()
Set m_Features = New Features
End Sub
Public Property Get Features() As Features
Set Features = m_Features
End Property
Features Class
Option Explicit
Private m_Hair As String 'this would actually be another class
'in your example
Public Property Get Hair() As String
Hair = m_Hair
End Property
Public Property Let Hair(ByVal Value As String)
m_Hair = Value
End Property

How to save all class hardcoded values in one place

I have created a class in VBA which I would like to have some pre-set values associated with it. I am new to classes, and am wondering what is the best (/a good) way of structuring my VBA code within the class object, so that I can access these default values easily as I type. An answer should preferably:
Require relatively few additional lines of code over and above the lines which I assume will be required for the actual hard-coding of values
ie. something like an additional Sub for each hardcoded value would not be ideal
this is to prevent my class from becoming too cluttered
Allow me to use intellisense in some way to access these hard coded values
It's worth noting that my main use of these hard coded values is in setting default values to variables of my class (by looping in the initialize event), but I may also want to access them in other portions of code
What I've tried:
Declaring an Enum to save my hard coded values
'Declarations
Private Enum startingVals
Top = 10
Column_Count = 4
Left = 15
...
End Enum
Private topVal As Long 'variables which I assign default values to
Private colCnt As Long
Private leftVal As Long
Private Sub Class_Initialize()
topVal = startingVals.Top
colCnt = startingVals.Column_Count
'etc.
End Sub
This has 2 limitations;
Enums can only store Longs
Get around this by using a load of Consts instead, but then you have to remember every constant's name, plus it looks cluttered in code
Although I get Intellisense for .Top and .Column_Count, I still have to type startingVals out in full
That's significantly better than having to remember all the hardcoded constant names though
Ideally I would be able to do this
Private Sub Class_Initialize()
With startingVals 'or Dim v As startingVals, With v
topVal = .Top
colCnt = .Column_Count
'etc.
End With
End Sub
But I can't
Another approach would be to use a function to save the values,that way you could declare different types to just long.
'Declarations
Private Enum startingVals
Top = 1
Column_Count = 2
Left = 3
...
End Enum
Private topVal As Long 'variables which I assign default values to
Private colCnt As Long
Private leftVal As Long
Private Sub Class_Initialize()
topVal = getval(Top)
colCnt = getval(Column_Count)
'etc.
End Sub
Then to access the hard coded data, you have a function which takes an enum input (allowing for intellisense)
Private Function getval(dataType As startVals) As String
Const savedData As String = "1,2,1.17171717,hey,me,you" 'save the return values for the index specified by dataType
getval = Split(savedData, ",")(dataType) 'use datatype as a direct index of the array
End Function
or another way of saving the values
Private Function getval(dataType As startVals) As String
Const colV As Long = 10 'index 1
Const topV As String = "This is the top" 'index 2
'...
If dataType = ColumnCount Then getval = colV 'use dataType to check what to return
If dataType = Top Then getval = colV 'could use a select case too
'etc
End Function
But either way we still can't access the constants unless we type the function name out.
Also this approach requires me to update both the enum declaration at my class declarations portion, and the const declaration within the function itself, making the code harder to maintain.
TL;DR
What's the best method to save hardcoded values in a class object, where best is defined as
Uses VBA intellisense (autofill) so I can quickly select the value I want as I type
Is neat, self contained and concise within my class module, to avoid clutter
Can preferably hold any kind (data type) of hardcoded value (although I am only using Long in the project I'm currently working on)
Can be accessed without the need of typing an initialisation portion each time (such as a function or enum name)
Of course a With block or function equivalent would be fine, as that only requires the one instance of specifying the enum/data collection name
...to prevent my class from becoming too cluttered
I would separate the class from its initialization process adding another class lets call it Initializer. Initializer will know how to initialize my objects, will contain the default values and will fill my object with this defaults. But in the initializer you will have to write the assignments, no magic intellisense, but just simply write m_ and select from the list. HTH
Class Foo
Option Explicit
'variables which I assign default values to
Private m_topVal As Long
Private m_colCnt As Long
'Private m_leftVal As Long
Private Sub Class_Initialize()
Dim initializer As FooInitializer
Set initializer = New FooInitializer
initializer.Initialize Me
End Sub
Public Property Get TopVal() As Long
TopVal = m_topVal
End Property
Public Property Let TopVal(ByVal vNewValue As Long)
m_topVal = vNewValue
End Property
Public Property Get ColCnt() As Long
ColCnt = m_colCnt
End Property
Public Property Let ColCnt(ByVal vNewValue As Long)
m_colCnt = vNewValue
End Property
' Add Get/Let(Set) for other member variables as well
Class FooInitializer
Option Explicit
' Default startingVals values
Private m_topValDefault As Integer
Private m_columnCountDefault As Integer
'etc.
Public Sub Initialize(ByRef fooInstance As Foo)
fooInstance.TopVal = m_topValDefault
fooInstance.ColCnt = m_columnCountDefault
'etc.
End Sub
Private Sub Class_Initialize()
m_topValDefault = 10
m_columnCountDefault = 4
'etc.
End Sub
Standard module
Option Explicit
Sub test()
Dim f As Foo
Set f = New Foo
' f is now initizlized via initializer with default values
Debug.Print f.TopVal
Debug.Print f.ColCnt
End Sub
You can use constants to define the default values in a single place.
You can then easily access them with Ctrl + Space + Default...
Const Default_Top = 10
Const Default_Text = "abcd"
Private m_topVal As Long
Private m_text As String
Private Sub Class_Initialize()
m_topVal = Default_Top
m_text = Default_Text
End Sub
Public Property Get TopVal() As Long
TopVal = m_topVal
End Property
I can't claim ownership to this solution, but when I ran into it over on Code Review it was genius enough for me to have incorporated it into quite a lot of my code since.
As used in some other object-oriented languages, accessing class-internal instance variables using a this construct is very familiar. The concept is extended into VBA using the example here.
I've created a class module called CustomClass, and within it created a private custom type for use only within that class.
Option Explicit
Private Type CustomType
Top As Long
Name As String
Temperature As Double
anotherCustomObject As CustomClass
End Type
Private this As CustomType
Working this way, you can create any number of internal variables of any combination of types (including objects). Accessing and initializing each of these values is now as simple as using the this structured variable. The Class_Initialize sub shows how:
Private Sub Class_Initialize()
this.Top = 150
this.Name = "Wayne"
this.Temperature = 98.6
Set this.anotherCustomObject = New CustomClass
End Sub
Set and initialize all your values to your heart's content.
Further, you can establish each with property accessors if you like. Some of these can be Read Only:
'--- Read Only Properties
Public Property Get Name() As String
Name = this.Name
End Property
Public Property Get Temperature() As Double
Temperature = this.Temperature
End Property
Public Property Get ContainedObject() As CustomClass
Set ContainedObject = this.anotherCustomObject
End Property
And you can create some that are Read/Write:
'--- Read/Write Properties
Public Property Let Top(ByVal newValue As Long)
this.Top = newValue
End Property
Public Property Get Top() As Long
Top = this.Top
End Property
Plus, you can still use the properties easily within the class using the Me keyword:
'--- Internal Private Methods
Private Sub TestThisClass()
Debug.Print "current temperature is " & Me.Temperature
Debug.Print "the Top value is " & Me.Top
End Sub
Of course, this all works when you declare an object of CustomClass in a different module as well.
Hopefully this helps goes a ways to helping regularize your code a bit.
(For convenience, here's the whole class:)
Option Explicit
Private Type CustomType
Top As Long
Name As String
Temperature As Double
anotherCustomObject As CustomClass
End Type
Private this As CustomType
Private Sub Class_Initialize()
this.Top = 150
this.Name = "Wayne"
this.Temperature = 98.6
Set this.anotherCustomObject = New CustomClass
End Sub
'--- Read Only Properties
Public Property Get Name() As String
Name = this.Name
End Property
Public Property Get Temperature() As Double
Temperature = this.Temperature
End Property
Public Property Get ContainedObject() As CustomClass
Set ContainedObject = this.anotherCustomObject
End Property
'--- Read/Write Properties
Public Property Let Top(ByVal newValue As Long)
this.Top = newValue
End Property
Public Property Get Top() As Long
Top = this.Top
End Property
'--- Internal Private Methods
Private Sub TestThisClass()
Debug.Print "current temperature is " & Me.Temperature
Debug.Print "the Top value is " & Me.Top
End Sub

Sub function to show UserForm

I have an excel file with multiple UserForms. To open a UserForm I have code such as
Sub runAdjuster()
Adjuster.Show
End Sub
There are about 5 of these. What is considered best practice in terms of where this code should be kept? I originally had it in a module, but have decided to move it to the ThisWorkbook object. Looking for tips on what is normally done to keep code clean.
Assuming Adjuster is the name of the form, you're using the default instance here, which isn't ideal.
This would already be better:
Dim view As Adjuster
Set view = New Adjuster
view.Show
Yes, it's more code. But you're using a dedicated object (i.e. view) and, if that object's state gets modified, these changes aren't going to affect the default instance. Think of that default instance as a global object: it's global, which isn't very OOP.
Now, you may argue, why not "new up" the object on the same line as the declaration then?
Consider this:
Sub DoSomething()
Dim c As New Collection
Set c = Nothing
c.Add "test"
End Sub
Is this code accessing a null reference and blowing up with a run-time error 91? No! Confusing? Yes! Hence, avoid the As New shortcut, unless you like having VBA automagically doing implicit stuff behind your back.
So, you're asking about best practice... I tend to consider VBA UserForms as an early pre-.NET version of winforms, and best practice design pattern for WinForms is the Model-View-Presenter pattern (aka "MVP").
Following this pattern, you'll have UserForms strictly responsible for presentation, and you'll have your business logic either implemented in a presenter object, or in a dedicated object that the presenter uses. Something like this:
Class Module: MyPresenter
The presenter class receives events from the model, and executes application logic depending on the state of the model. It knows about a concept of a view, but it doesn't have to be tightly coupled with a concrete implementation (e.g. MyUserForm) - with proper tooling you could write unit tests to validate your logic programmatically, without having to actually run the code and display the form and click everywhere.
Option Explicit
Private Type TPresenter
View As IView
End type
Public Enum PresenterError
ERR_ModelNotSet = vbObjectError + 42
End Enum
Private WithEvents viewModel As MyModel
Private this As TPresenter
Public Sub Show()
If viewModel Is Nothing Then
Err.Raise ERR_ModelNotSet, "MyPresenter.Show", "Model is not set to an object reference."
End If
'todo: set up model properties
view.Show
If Not view.IsCancelled Then DoSomething
End Sub
Public Property Get View() As IView
Set View = this.View
End Property
Public Property Set View(ByVal value As IView)
Set this.View = value
If Not this.View Is Nothing Then Set this.View.Model = viewModel
End Property
Public Property Get Model() As MyModel
Set Model = viewModel
End Property
Public Property Set Model(ByVal value As MyModel)
Set viewModel = value
If Not this.View Is Nothing Then Set this.View.Model = viewModel
End Property
Private Sub Class_Terminate()
Set this.View.Model = Nothing
Set this.View = Nothing
Set viewModel = Nothing
End Sub
Private Sub viewModel_PropertyChanged(ByVal changedProperty As ModelProperties)
'todo: execute logic that needs to run when something changes in the form
End Sub
Private Sub DoSomething()
'todo: whatever needs to happen after the form closes
End Sub
Class Module: IView
That's the abstraction that represents the concept of a View that exposes everything the Presenter needs to know about any UserForm - note that everything it needs to know, isn't much:
Option Explicit
Public Property Get Model() As Object
End Property
Public Property Set Model(ByVal value As Object)
End Property
Public Property Get IsCancelled() As Boolean
End Property
Public Sub Show()
End Sub
Class Module: MyModel
The model class encapsulates the data that the form needs and manipulates. It doesn't know about the view, and it doesn't know about the presenter either: it's just a container for encapsulated data, with simple logic that enables both the view and the presenter to execute code when any of the properties are modified.
Option Explicit
Private Type TModel
MyProperty As String
SomeOtherProperty As String
'todo: wrap members here
End Type
Public Enum ModelProperties
MyProperty
SomeOtherProperty
'todo: add enum values here for each monitored property
End Enum
Public Event PropertyChanged(ByVal changedProperty As ModelProperties)
Private this As TModel
Public Property Get MyProperty() As String
MyProperty = this.MyProperty
End Property
Public Property Let MyProperty(ByVal value As String)
If this.MyProperty <> value Then
this.MyProperty = value
RaiseEvent PropertyChanged(MyProperty)
End If
End Property
Public Property Get SomeOtherProperty() As String
SomeProperty = this.SomeOtherProperty
End Property
Public Property Let SomeOtherProperty(ByVal value As String)
If this.SomeOtherProperty <> value Then
this.SomeOtherProperty = value
RaiseEvent PropertyChanged(SomeOtherProperty)
End If
End Property
'todo: expose other model properties
UserForm: MyUserForm
The UserForm is strictly responsible for visual presentation; all its event handlers to, is change the value of a property in the model - the model then tells the presenter "hey I've been modified!", and the presenter acts accordingly. The form also listens for modified properties on the model, so when the presenter changes the model, the view can execute code and update itself accordingly. Here's an example of a simple form "binding" the MyProperty model property to the text of some TextBox1; I added a listener for SomeOtherProperty just to illustrate that the view can also be updated indirectly when the model changes.
Obviously the view wouldn't be reacting to the same properties changing as the presenter, otherwise you would enter an endless ping-pong of callbacks that would eventually blow up the stack... but you get the idea.
Note that the form implements the IView interface, so that the presenter can talk to it without actually knowing about its inner workings. The interface implementation simply refers to concrete members, but the concrete members don't even need to actually exist, since they won't even be used!
Option Explicit
Implements IView
Private Type TView
IsCancelled As Boolean
End Type
Private WithEvents viewModel As MyModel
Private this As TView
Private Property Get IView_Model() As Object
Set IView_Model = Model
End Property
Private Property Set IView_Model(ByVal value As Object)
Set Model = value
End Property
Private Property Get IView_IsCancelled() As Boolean
IView_IsCancelled = IsCancelled
End Property
Private Sub IView_Show()
Show vbModal
End Sub
Public Property Get Model() As MyModel
Set Model = viewModel
End Property
Public Property Set Model(ByVal value As MyModel)
Set viewModel = value
End Property
Public Property Get IsCancelled() As Boolean
IsCancelled = this.IsCancelled
End Property
Private Sub CancelButton_Click()
this.IsCancelled = True
Me.Hide
End Sub
Private Sub OkButton_Click()
Me.Hide
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
'"x-ing out" of the form is like clicking the Cancel button
If CloseMode = VbQueryClose.vbFormControlMenu Then
this.IsCancelled = True
End If
End Sub
Private Sub UserForm_Activate()
If viewModel Is Nothing Then
MsgBox "Model property must be assigned before the view can be displayed.", vbCritical, "Error"
Unload Me
Else
Me.TextBox1.Text = viewModel.MyProperty
Me.TextBox1.SetFocus
End If
End Sub
Private Sub TextBox1_Change()
'UI elements update the model properties
viewModel.MyProperty = Me.TextBox1.Text
End Sub
Private Sub viewModel_PropertyChanged(ByVal changedProperty As ModelProperties)
If changedProperty = SomeOtherProperty Then
Frame1.Caption = SomeOtherProperty
End If
End Sub
Module: Macros
Say your spreadsheet had a shape and you wanted to run that logic when it's clicked. You need to attach a macro to that shape - I like to regroup all macros in a standard module (.bas) called "Macros", that contains nothing but public procedures that all look like this:
Option Explicit
Public Sub DoSomething()
Dim presenter As MyPresenter
Set presenter = New MyPresenter
Dim theModel As MyModel
Set theModel = New MyModel
Dim theView As IView
Set theView = New MyUserForm
Set presenter.Model = theModel
Set presenter.View = theView
presenter.Show
End Sub
Now, if you want to test your presenter logic programmatically without showing a form, all you need to do is implement a "fake" view, and write a test method that will do what you need:
Class: MyFakeView
Option Explicit
Implements IView
Private Type TFakeView
IsCancelled As Boolean
End Type
Private this As TFakeView
Private Property Get IView_Model() As Object
Set IView_Model = Model
End Property
Private Property Set IView_Model(ByVal value As Object)
Set Model = value
End Property
Private Property Get IView_IsCancelled() As Boolean
IView_IsCancelled = IsCancelled
End Property
Private Sub IView_Show()
IsCancelled = False
End Sub
Public Property Get IsCancelled() As Boolean
IsCancelled = this.IsCancelled
End Property
Public Property Let IsCancelled(ByVal value As Boolean)
this.IsCancelled = value
End Property
Module: TestModule1
There are probably other tools out there, but since I actually wrote this one and I like how it works without a crap ton of boilerplate setup code or comments that contain executable instructions I'm going to warmly recommend using Rubberduck unit tests. Here's what a [very simple] test module might look like:
'#TestModule
Option Explicit
Option Private Module
Private Assert As New Rubberduck.AssertClass
'#TestMethod
Public Sub Model_SomePropertyInitializesEmpty()
On Error GoTo TestFail
'Arrange
Dim presenter As MyPresenter
Set presenter = New MyPresenter
Dim theModel As MyModel
Set theModel = New MyModel
Set presenter.Model = theModel
Set presenter.View = New MyFakeView
'Act
presenter.Show
'Assert
Assert.IsTrue theModel.SomeProperty = vbNullString
TestExit:
Exit Sub
TestFail:
Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub
Rubberduck unit tests allow you to use this decoupled code to test everything you want to test about your application logic - as long as you keep that application logic decoupled and that you write testable code, you'll have unit tests that document how your VBA application is supposed to behave, tests that document what the specs are - just like you would have them in C# or Java, or any other OOP language one can write unit tests with.
Point is, VBA can do it, too.
Overkill? Depends. Specs changes all the time, code changes accordingly. Implementing all the application logic in spreadsheets' code-behind gets utterly annoying, because the Project Explorer doesn't drill down to module members, so finding what's implemented where can easily get annoying.
And it's even worse when the logic is implemented in the forms' code-behind and then you have Button_Click handlers making database calls or spreadsheet manipulations.
Code that's implemented in objects that have as few responsibilities as possible, makes code that's reusable, and that's easier to maintain.
Your question isn't exactly precise about exactly what you mean with "an Excel file with multiple userforms", but if you need to, you could have a "main" presenter class that receives 4-5 "child" presenters, each being responsible for the specific logic tied to each "child" form.
That said, if you have working code (that works exactly as intended) that you would like to refactor and make more efficient, or easier to read/maintain, you can post it on Code Review Stack Exchange, that's what that site is for.
Disclaimer: I maintain the Rubberduck project.
It depends on what launches these subs. If they are attached to a button or shape (which is what I tend to do for launching userforms) then it makes sense to put them in the module for the sheet that contains the shape. If buttons/shapes on several sheets refer to it -- put them in a general code module. I don't know if there really is a "best practice" here. The most important thing is to have consistency so that you don't have to go searching for things.

Using collections as properties of an object in VBA

With class module "Class1Test" as
Private pGreetings As Collection
Public Property Get Greetings() As Collection
Greetings = pGreetings
End Property
Public Property Let Greetings(Value As Collection)
pGreetings = Value
End Property
If I run the sub
Dim MyPhrases As Class1Test
Public Sub Test()
Set MyPhrases = New Class1Test
MyPhrases.Greetings.Add "Have a nice day"
End Sub
I get the a compile error "Argument not optional"
Why can't I add the string to the the collection myphrases.greetings ? Please forgive the newbie question. Just learning VBA.
A few things wrong.
Collection is an object, so you must use the Set keyword when assigning. Also in the Let procedure for consistency in naming conventions, I would use lGreetings instead of Value although that should not really matter.
Private pGreetings As Collection
Public Property Get Greetings() As Collection
Set Greetings = pGreetings
End Property
Public Property Let Greetings(lGreetings As Collection)
Set pGreetings = lGreetings
End Property
This will still raise an 91 error (Object variable or with block not set) because you have not instantiated the collection object. Probably the way you should do this is in the class module's Initialize routine.
Private Sub Class_Initialize()
Set pGreetings = New Collection
End Sub