How to incorporate Excel VBA class collection into interface/factory method? - excel

I've been using class modules for almost a year, and I'm just now comfortable with them. Now I'm trying to incorporate factory methods into data extraction from workbook tables. I found some great guides on the topic here, here, and here, but I'm unsure where to incorporate a collection of the class.
Up until now, I've setup my class modules with self-contained collections in this format:
Class module OrigClass
Option Explicit
'Col position references for input table, only includes cols with relevant data
Private Enum icrColRef
icrName = 2
icrCost = 4
End Enum
'UDT mirrors class properties
Private Type TTestClass
Name As String
Cost As Long
End Type
Const WS_NAME As String = "Sheet1"
Const NR_TBL As String = "Table1"
Private msTestClass As Collection
Private TestClass As TTestClass
Private Sub Class_Initialize()
Set msTestClass = New Collection
End Sub
Public Sub Add(Item As OrigClass)
msTestClass.Add _
Item:=Item, _
Key:=Item.Name
End Sub
Public Function Extract() As OrigClass
Dim tblInputs As ListObject
Dim i As Integer
Dim Item As OrigClass
Set tblInputs = ThisWorkbook.Worksheets(WS_NAME).ListObjects(NR_TBL)
For i = 1 To tblInputs.DataBodyRange.Rows.Count
Set Item = New OrigClass
With Item
.Name = tblInputs.DataBodyRange(i, icrName).Value
.Cost = tblInputs.DataBodyRange(i, icrCost).Value
End With
msTestClass.Add Item
Next i
End Function
Public Function Item(i As Variant) As OrigClass
Set Item = msTestClass.Item(i)
End Function
Public Function Count() As Integer
Count = msTestClass.Count
End Function
Friend Property Let Name(Val As String)
TestClass.Name = Val
End Property
Public Property Get Name() As String
Name = TestClass.Name
End Property
Friend Property Let Cost(Val As Long)
TestClass.Cost = Val
End Property
Public Property Get Cost() As Long
Cost = TestClass.Cost
End Property
This structure works well when I build functions that pass a ranges/table, loop through the rows, and assign a column value to each property. The address is almost always constant and only the values and record count will vary.
I just started building an interface for a class while also trying to retain the collection component, but I'm stumbling on runtime errors... I could possibly create a separate collection class, but I think my problem is more about mismanaging scope rather than encapsulation:
Class module CTestClass
Option Explicit
'Col position references for input table, only includes cols with relevant data
Private Enum icrColRef
icrName = 2
icrCost = 4
End Enum
''UDT mirrors class properties
Private Type TTestClass
Name As String
Cost As Long
End Type
Const WS_NAME As String = "Sheet1"
Const NR_TBL As String = "Table1"
Private msTestClass As Collection
Private TestClass As TTestClass
Implements ITestClass
Implements FTestClass
Private Sub Class_Initialize()
Set msTestClass = New Collection
End Sub
Public Sub Add(Item As CTestClass)
msTestClass.Add _
Item:=Item, _
Key:=Item.Name
End Sub
Public Function Create() As ITestClass
With New CTestClass
.Extract
' 2) now in Locals window, Me.msTestClass is <No Variables>
Set Create = .Self
' 4) Me.msTestClass is again <No Variables>, and
' Create (as Type ITextClass) is Nothing
' Create (as Type ITextClass/ITextClass) lists property values as
' <Object doesn't support this property or method>, aka runtime error 438
End With
End Function
Private Function FTestClass_Create() As ITestClass
Set FTestClass_Create = Create
End Function
Public Function Extract() As ITestClass
Dim tblInputs As ListObject
Dim i As Integer
Dim Item As CTestClass
Set tblInputs = ThisWorkbook.Worksheets(WS_NAME).ListObjects(NR_TBL)
For i = 1 To tblInputs.DataBodyRange.Rows.Count
Set Item = New CTestClass
With Item
.Name = tblInputs.DataBodyRange(i, icrName).Value
.Cost = tblInputs.DataBodyRange(i, icrCost).Value
End With
msTestClass.Add Item
Next i
' 1) in Locals window, Me.msTestClass is populated with all table records
End Function
Public Function ITestClass_Item(i As Variant) As ITestClass
Set ITestClass_Item = msTestClass.Item(i)
End Function
Public Function ITestClass_Count() As Integer
ITestClass_Count = msTestClass.Count
End Function
Friend Property Let Name(Val As String)
TestClass.Name = Val
End Property
Public Property Get Name() As String
Name = TestClass.Name
End Property
Friend Property Let Cost(Val As Long)
TestClass.Cost = Val
End Property
Public Property Get Cost() As Long
Cost = TestClass.Cost
End Property
Public Property Get Self() As ITestClass
Set Self = Me
' 3) Me.msTestClass is again populated with all table records (scope shift?), but
' Self is set to Nothing
End Property
Private Property Get ITestClass_Name() As String
ITestClass_Name = Name
End Property
Private Property Get ITestClass_Cost() As Long
ITestClass_Cost = Cost
End Property
Interface module ITestClass
'Attribute VB_PredeclaredId = False <-- revised in text editor
Option Explicit
Public Function Item(i As Variant) As ITestClass
End Function
Public Function Count() As Integer
End Function
Public Property Get Name() As String
End Property
Public Property Get Cost() As Long
End Property
Factory module FTestClass
'Attribute VB_PredeclaredId = False <-- revised in text editor
Option Explicit
Public Function Create() As ITestClass
End Function
Standard module
Sub TestFactory()
Dim i As ITestClass
Dim oTest As FTestClass
Set oTest = CTestClass.Create
' 5) oTest is <No Variables>, no properties are present
' as if the variable was never set
For Each i In oTest ' <-- Runtime error 438, Object doesn't support this property or method
Debug.Print
Debug.Print i.Name
Debug.Print i.Cost
Next i
End Sub
What am I doing wrong here?
EDIT:
#freeflow pointed out that I didn't state my intentions for introducing an interface.
My office uses several workbook "models" to compile pricing data into a single output table that is then delivered to a downstream customer for importing into a database.
My goal is to standardize the calculations using these various models. The side goal is to understand how to properly implement a factory method.
Each model has one or more input tables, and each table contains a unique collection of 10-30 fields/columns. The output data calculations vary, along with the dependencies on various input fields. However, the output data is the same format all across the board and always contains the same dozen fields.
The example I've shown is intended to be a single interface ITestClass for writing data to the output table. The class that implements it CTestClass can be considered as just one of the several tables (within the several models) containing the input data. I plan on modeling more class objects, one for each input table.

Based on:
Sub TestFactory()
Dim i As ITestClass
Dim oTest As FTestClass
Set oTest = CTestClass.Create
' 5) oTest is <No Variables>, no properties are present
' as if the variable was never set
For Each i In oTest ' <-- Runtime error 438, Object doesn't support this property or method
Debug.Print
Debug.Print i.Name
Debug.Print i.Cost
Next i
End Sub
It would appear that you are interested in making your class iterable like a collection. I would point you towards this SO question. The short of it is...it's difficult.
WIth regard to the error: The result of statement Set oTest = CTestClass.Create is the acquisition of a FTestClass interface that exposes a single method: Public Function Create() As ITestClass. Which, provides nothing to iterate on and results in an error.
Other Observations:
In the code as provided, there is no need to declare a factory interface.
(Sidebar: Interface classes typically begin with the letter "I". In this case, a better interface name for FTestClass would be "ITestClassFactory")
Since CTestClass has its VB_PredeclaredId attribute set to 'True', any Public method (or field) declared in CTestClass is exposed...and is considered its default interface. CTestClass.Create() is the Factory method you are interested in.
One purpose of creating a Factory method (in VBA) is to support the parameterized creation of a class instance. Since the Create function currently has no parameters, it is unclear what else could be going on during creation other than Set tClass = new CTestClass. But, there are parameters that would indicate what is going on during Create.
Public Function Create(ByVal tblInputs As ListObject, OPtional ByVal nameColumn As Long = 2, Optional ByVal costColumn As Long = 4) As ITestClass
In other words, CTestClass has a dependency on a ListObject in order to become a valid instance of a CTestClass. A factory method's signature typically contains dependencies of the class. With the above factory method, there is no longer a need to have an Extract function - Public or otherwise. Notice also (in the code below) that the ThisWorkbook reference is no longer part of the object. Now, the tblInputs ListObject can be from anywhere. And the important column numbers can be easily modified. This parameter list allows you to test this class using worksheets with fake data.
Reorganizing:
CTestClass contains a Collection of CTestClass instances. It would seem clearer to declare a TestClassContainer class that exposes the Create function above. The container class can then expose a NameCostPairs property which simply exposes the msTestClass Collection. Creating a container class reduces the TestClass to essentially a data object (all Properties, no methods) which results in a useful separation of concerns. Let the calling objects handle the iteration of the collection.
TestClassContainer
Option Explicit
Private Type TTestClassContainer
msTestClass As Collection
End Type
Private this As TTestClassContainer
'TestContainer Factory method
Public Function Create(ByVal tblInputs As ListObject, Optional ByVal nameCol As Long = 2, Optional ByVal costCol As Long = 4) As TestClassContainer
Dim i As Integer
Dim nameCostPair As CTestClass
Dim newInstance As TestClassContainer
With New TestClassContainer
Set newInstance = .Self
For i = 1 To tblInputs.DataBodyRange.Rows.Count
Set nameCostPair = New CTestClass
nameCostPair.Name = tblInputs.DataBodyRange(i, nameCol).Value
nameCostPair.Cost = tblInputs.DataBodyRange(i, costCol).Value
newInstance.AddTestClass nameCostPair
Next i
End With
Set Create = newInstance
End Function
Public Sub AddTestClass(ByVal tstClass As CTestClass)
this.msTestClass.Add tstClass
End Sub
Public Property Get Self() As CTestClass
Set Self = Me
End Property
Public Property Get NameCostPairs() As Collection
Set NameCostPairs = this.msTestClass
End Property
CTestClass (no longer needs VB_PredeclaredId set to 'True')
Option Explicit
Implements ITestClass
''UDT mirrors class properties
Private Type TTestClass
Name As String
Cost As Long
End Type
Private this As TTestClass
Public Property Let Name(Val As String)
this.Name = Val
End Property
Public Property Get Name() As String
Name = this.Name
End Property
Public Property Let Cost(Val As Long)
this.Cost = Val
End Property
Public Property Get Cost() As Long
Cost = this.Cost
End Property
Private Property Get ITestClass_Name() As String
ITestClass_Name = Name
End Property
Private Property Get ITestClass_Cost() As Long
ITestClass_Cost = Cost
End Property
And Finally:
Option Explicit
Sub TestFactory()
Const WS_NAME As String = "Sheet1"
Const NR_TBL As String = "Table1"
Dim tblInputs As ListObject
Set tblInputs = ThisWorkbook.Worksheets(WS_NAME).ListObjects(NR_TBL)
Dim container As TestClassContainer
Set container = TestClassContainer.Create(tblInputs)
Dim nameCostPair As ITestClass
Dim containerItem As Variant
For Each containerItem In container.NameCostPairs
Set nameCostPair = containerItem
Debug.Print
Debug.Print nameCostPair.Name
Debug.Print nameCostPair.Cost
Next
End Sub

I see #BZgr has provided a solution but as I'd also written one I provide the answer below as analternative.
I think there are several problems with th OP code.
The origclass and collection of origclasses is conflated, they should be separate. Disentangling this wasn't made easier by the poor naming of the origclass UDT.
Its not clear what needs to be a factory. I've put the factory method in the origclasses class so that an 'immutable' collection of origclass is created.
Its not clear what the op is trying to achieve by introducing an interface. In general, interfaces are used when a number of different object must provide that same set of methods. In VBA the interface declaration allows the compiler to check if each object that claims to implement the interface has the correct methods and parameter lists. (but i do accept that there may be some special VBA cases where this is not the case)
The code below compiles and has no significant Rubberduck inspections. However, I am not a user of Excel VBA so I apologise in advance if my code makes mistakes in this area.
a. We have a separate and very simple OrigClass
Option Explicit
Private Type Properties
Name As String
Cost As Long
End Type
Private p As Properties
Public Property Get Name() As String
Name = p.Name
End Property
Public Property Let Name(ByVal ipString As String)
p.Name = ipString
End Property
Public Property Get Cost() As Long
Cost = p.Cost
End Property
Public Property Let Cost(ByVal ipCost As Long)
p.Cost = ipCost
End Property
2 The OrigClaases class which is a collection of origclass
Option Explicit
'#PredeclaredId
'#Exposed
'Col position references for input table, only includes cols with relevant data
Private Enum icrColRef
icrName = 2
icrCost = 4
End Enum
Private Type State
'TestClass As Collection
Host As Collection
ExternalData As Excel.Worksheet
TableName As String
End Type
Private s As State
Public Function Deb(ByVal ipWorksheet As Excel.Worksheet, ByVal ipTableName As String) As OrigClasses
With New OrigClasses
Set Deb = .ReadyToUseInstance(ipWorksheet, ipTableName)
End With
End Function
Friend Function ReadyToUseInstance(ByVal ipWorksheet As Excel.Worksheet, ByVal ipTableName As String) As OrigClasses
Set s.Host = New Collection
Set s.ExternalData = ipWorksheet
s.TableName = ipTableName
PopulateHost
Set ReadyToUseInstance = Me
End Function
' The fact that you are using the collection Key suggests
' you might be better of using a scripting.dictioanry
' Also given that you populate host doirectly from the worksheet
' this add method may now be redundant.
Public Sub Add(ByVal ipItem As OrigClass)
s.Host.Add _
Item:=ipItem, _
Key:=ipItem.Name
End Sub
Public Sub Extract()
' Extract is restricted to re extracting data
' should the worksheet have been changed.
' If you need to work on a new sheet then
' create a new OrigClasses object
Set s.Host = New Collection
PopulateHost
End Sub
Private Sub PopulateHost()
Dim tblInputs As ListObject
Set tblInputs = s.ExternalData.ListObjects(s.TableName)
Dim myRow As Long
For myRow = 1 To tblInputs.DataBodyRange.Rows.Count
Dim myItem As OrigClass
Set myItem = New OrigClass
With myItem
.Name = tblInputs.DataBodyRange(myRow, icrName).Value
.Cost = tblInputs.DataBodyRange(myRow, icrCost).Value
End With
s.Host.Add myItem, myItem.Name
Next
End Sub
Public Function Item(ByVal ipIndex As Variant) As OrigClass
Set Item = s.Host.Item(ipIndex)
End Function
Public Function Count() As Long
Count = s.Host.Count
End Function
Public Function Name(ByVal ipIndex As Long) As String
Name = s.Host.Item(ipIndex).Name
End Function
Public Function Cost(ByVal ipIndex As Long) As Long
Cost = s.Host.Item(ipIndex).Cost
End Function
Public Function SheetName() As String
SheetName = s.ExternalData.Name
End Function
Public Function TableName() As String
TableName = s.TableName
End Function
'#Enumerator
Public Function NewEnum() As IUnknown
Set NewEnum = s.Host.[_NewEnum]
End Function
c. The testing code
Option Explicit
Const WS_NAME As String = "Sheet1"
Const NR_TBL As String = "Table1"
Sub TestFactory()
Dim oTest As OrigClasses
'#Ignore UnassignedVariableUsage
Set oTest = OrigClasses.Deb(ThisWorkbook.Worksheets(WS_NAME), NR_TBL)
Dim myOrigClass As Variant
For Each myOrigClass In oTest
Debug.Print
Debug.Print myOrigClass.Name
Debug.Print myOrigClass.Cost
Next
End Sub
For the factory method, following feeback from Rubberduck, I now use the method name 'Deb' which is short for Debut (or Debutante) meaning something that is presented which is ready to be used. Which of course leads to why I use the method name 'readytoUseInstance'.
I Use UDT of Properties and State (with variables p and s) to separate extenal properties from internal state.
Within methods I prefix variables with the prefix 'my'.
For method parameters i use the prefixed ip, op and iop for input only, output only, and imput that is mutated and output.
A side benefit of these prefixes p,s,my,ip,op,iop is that they also remove some the majority of the issues encountered when trying to name variables/parameters.

Related

Call an Item from a Collection of Items

I have a class "Currency" and a class "Currencies" which is a collection of instances of "Currency" class objects. I think this resembles Excel's Worksheet class which is a member of the Sheets collection. I can address any member by index or "Key", like Sheets(1) or `Sheets("Sheet1").
Here is code from my "Currencies" class module. It's abbreviated for use here and may not run, which is not the issue.
Private Sub Class_Initialize()
' Class "Currencies"
Dim R As Long
Set AllCcys = New Collection
Arr = .Range("Currencies").Value
For R = 1 To UBound(Arr)
Set Ccy = Me.Add(Arr(R, 1))
Next R
End Sub
Public Function Add(ByVal Key As String) As cCcy
Dim Fun As cCcy
Set Fun = New cCcy
AllCcys.Add Fun, Key
Fun.Key = Key
Set Add = Fun
End Function
Public Property Get Item(Key As Variant) As cCcy
Set Item = AllCcys(Key)
End Property
With my setup I can access any of the "Currency" objects with syntax like Currencies.Item("USD").Rate or Currencies.Item(1).Rate which I consider convoluted. I would like to use Currencies("USD").Rate analog to what I do when accessing Excel's Sheets collection.
How can I achieve that?
According to http://www.cpearson.com/excel/DefaultMember.aspx, you can specify any procedure in a custom class to be the default member by the steps below:
Export Currencies class module
Open the exported module in Notepad
Add Attribute Value.VB_UserMemId = 0 in your Item property like this:
Public Property Get Item(Key As Variant) As cCcy
Attribute Value.VB_UserMemId = 0
Set Item = AllCcys(Key)
End Property
Save and Import back into your file.
Note: You can only have 1 procedure be the default member and you will not see the line Attribute Value.VB_UserMemId = 0 in the VBE.

VBA - Trying to understand how to call Class Modules

I'm learning VBA through Google, YouTube, etc.. and I came across Class Modules.
I have a Tracker Template.
Every few days I get a report sent to me ("Ice cream FG Inv.xlsm")
While trying to understand Class Modules, I found a template that created a Class Module (within the Tracker Template) WBIceCreamFGINVxlsm creating a CodeName for all of the worksheets within the Ice Cream FG Inv.xlsm Workbook.
Example:
Public Property Get wsinventory() As Worksheet
Set wsinventory = Workbook.Worksheets("Inventory")
End Property
In my module, I want to reference wsinventory, but not understanding exactly how to 'call' the Class Module..
Both Workbooks are Open.
I tried to start with:
Dim Data As Variant
Data = wsinventory.Range("A1").CurrentRegion.Value (**Variable not Defined**)
Then I tried:
Dim wsinventory As Worksheets
With wsinventory
Dim Data As Variant
Data = .Range("A1").CurrentRegion.Value (**Object variable or With variable not set**)
End With
Do I still need to use:
Dim DataSource As Workbook
Set DataSource = Workbooks("Ice Cream FG Inv.xlsm")
With DataSource.Worksheets("Inventory")
End With
If so, what would be the reasoning for using Class Modules?
You need to create a class object before you can access the properties of that class.
Assuming you have this Class and naming it TestClass:
Private pwsinventory As Worksheet
Public Sub init()
Set pwsinventory = Worksheets("Inventory")
End Sub
Public Property Set wsinventory(lwsinventory As Worksheet)
Set pwsinventory = lwsinventory
End Property
Public Property Get wsinventory() As Worksheet
Set wsinventory = pwsinventory
End Property
You can set / get the properties like so:
Sub test()
Dim datacls As TestClass
Dim data As Worksheet
Set datacls = New TestClass
Set datacls.wsinventory = Worksheets("inventory")
Set data = datacls.wsinventory
Debug.Print data.Name
End Sub
This, however, is kind of weird and when you have a property you don't want to set (you need to pass an argument) you should use an initiate function. Unfortunately there is no way I know of to do this without manually calling that sub after the class object is created.
Sub Test2()
Dim datacls As TestClass
Set datacls = New TestClass
datacls.init
Debug.Print datacls.wsinventory.Name
End Sub
The most common case I use classes for is better containers. Generally storing many of the same class type inside an array / dictionary so it is clear what I'm calling, especially if I need to modify the data in the same manner for each instance.
I am going to give another example. Create a class definition and name it ArrayData, and define multiple initialization subroutines
ArrayData.cls
Private m_data() As Variant
Private Sub Class_Initialize()
End Sub
Public Sub IntializeEmpty(ByVal rows As Long, ByVal columns As Long)
ReDim m_data(1 To count, 1 To columns)
End Sub
Public Sub InitializeFromRange(ByRef target As Range)
If target.rows.count > 1 Or target.columns.count > 1 Then
m_data = target.Value2
Else
ReDim m_data(1 To 1, 1 To 1)
m_data(1, 1) = target.Value
End If
End Sub
Public Sub InitializeFromArray(ByRef data() As Variant)
m_data = data
End Sub
Public Property Get RowCount() As Long
RowCount = UBound(m_data, 1) - LBound(m_data, 1) + 1
End Property
Public Property Get ColCount() As Long
ColCount = UBound(m_data, 2) - LBound(m_data, 2) + 1
End Property
Public Property Get Item(ByVal row As Long, ByVal col As Long) As Variant
Item = m_data(row, col)
End Property
Public Property Let Item(ByVal row As Long, ByVal col As Long, ByVal x As Variant)
m_data(row, col) = x
End Property
Module
To test the code in a code module initialize the class with the New keyword and then call one of the custom initialization subroutines.
Public Sub TestArray()
Dim arr As New ArrayData
arr.InitializeFromRange Sheet1.Range("A2").Resize(10, 1)
Dim i As Long
For i = 1 To arr.RowCount
Debug.Print arr.Item(i, 1)
Next i
End Sub
PS. Also read this article on how to designate one property as the default. In the example above if Item was the default property then you could write code such as
Debug.Print arr(5,2)
instead of
Debug.Pring arr.Item(5,2)

Techniques for binding object properties to Sheet Cells

Edit:
The three main things I'm looking to accomplish here are:
To be able to encapsulate properties/methods into a class (easy enough)
Use excel ranges as a user input for users to manipulate class property values.
(bonus) Send user changes back up to a database.
I've been playing with the idea of building something in vba that would allow me to bind an object's property(ies) to a Range. Basically turning a cell into a bound control.
Some basic requirements I might be after include:
A change to the object property would update the cell value
A change to the cell would update the object property
The object property may be bound/unbound without losing the value of the property.
My initial thought is to build a BindRange class that simply gets its value from a range and sets its value to that range.
BindRange.cls:
Option Explicit
Private p_BoundCell As Range
Public Property Get Value() As String
If Me.IsBound Then Value = p_BoundCell.Value
End Property
Public Property Let Value(Val As String)
If Me.IsBound Then p_BoundCell.Value = Val
End Property
Public Property Get IsBound() As Boolean
If BoundToDeletedCell Then
Set p_BoundCell = Nothing
End If
IsBound = Not (p_BoundCell Is Nothing)
End Property
Public Sub Bind(Cell As Range)
Set p_BoundCell = Cell(1, 1)
End Sub
Private Function BoundToDeletedCell() As Boolean
Dim sTestAddress As String
On Error Resume Next
TRY:
If p_BoundCell Is Nothing Then
Exit Function
'// returns false
End If
sTestAddress = p_BoundCell.Address
If Err.Number = 424 Then 'object required
BoundToDeletedCell = True
End If
End Function
Then, I can set up my custom object with a pair of fields to manage the updates. I would also need a method to expose setting the range to be bound.
TestObject.cls:
Option Explicit
Private p_BindId As BindRange
Private p_Id As String
Public Property Get Id() As String
If p_BindId.IsBound Then
p_Id = p_BindId.Value
End If
Id = p_Id
End Property
Public Property Let Id(Val As String)
p_Id = Val
If p_BindId.IsBound Then
p_BindId.Value = p_Id
End If
End Property
Public Sub Id_Bind(Cell As Range)
p_BindId.Bind Cell
End Sub
Private Sub Class_Initialize()
Set p_BindId = New BindRange
End Sub
Private Sub Class_Terminate()
Set p_BindId = Nothing
End Sub
This could be annoying because any property I want to make "Bindable" I'll have to manage Get/Set and Bind for each. I'm also not too sure if this will cause any memory issues: making class-properties with variant typed values....
Also considering building a service-like class that keeps track of objects and their bound ranges in a dictionary-like structure?
Anyways, just curious if anyone has done something like this before or if you have any thoughts on how you might design this.
Binding individual cells to properties would be very cumbersome. I think a better technique would be to create a table to act as a property sheet and a PropertySheetWatcher that raise a PropertyChange event.
Let's say for instance that we wanted to create a simple game on a userform call Stack OverKill. Our game will have its Hero Class and multiple Enemies classes (e.g. Turtle, Rhino, Wolf). Although each class has its own business logic they all share common properties (Name, HP, ClassName, Left, Right ...etc). Naturally, since they all sure the same basic set of properties they should all Implement a common Interface (e.g. CharacterInterface). The beauty of this is they can all share the same Property Sheet Table.
Mock Property Sheet Table
PropertySheetWatcher:Class
Private WithEvents ws As Worksheet
Public Table As ListObject
Public Event PropertyChange(ByVal PropertyName As String, Value As Variant)
Public Sub Init(ByRef PropertySheetTable As ListObject)
Set ws = PropertySheetTable.Parent
Set Table = PropertySheetTable
End Sub
Private Sub ws_Change(ByVal Target As Range)
Dim PropertyName As String
If Not Intersect(Target, Table.DataBodyRange) Then
PropertyName = Intersect(Target.EntireColumn, Table.HeaderRowRange).Value
RaiseEvent PropertyChange(PropertyName, Target.Value)
End If
End Sub
Public Sub UpdateProperty(ByVal PropertyName As String, Name As String, Value As Variant)
Application.EnableEvents = False
Dim RowIndex As Long
RowIndex = Table.ListColumns("Name").DataBodyRange.Find(Name).Row
Table.ListColumns(PropertyName).DataBodyRange.Cells(RowIndex).Value = Value
Application.EnableEvents = True
End Sub
Hero:Class
Implements CharacterInterface
Private Type Members
Name As String
HP As Single
ClassName As String
Left As Single
Right As Single
Top As Single
Bottom As Single
Direction As Long
Speed As Single
End Type
Private m As Members
Public WithEvents Watcher As PropertySheetWatcher
Private Sub Watcher_PropertyChange(ByVal PropertyName As String, Value As Variant)
Select Case PropertyName
Case "Speed"
Speed = Value
Case "HP"
'....More Code
End Select
End Sub
Public Property Get Speed() As Single
Speed = m.Speed
End Property
Public Property Let Speed(ByVal Value As Single)
m.Speed = Speed
Watcher.UpdateProperty "Speed", m.Name, Value
End Property
Private Property Get CharacterInterface_Speed() As Single
CharacterInterface_Speed = Speed
End Property
Private Property Let CharacterInterface_Speed(ByVal Value As Single)
Speed = Value
End Property
The classes above give are a quick muck-up of how the notification system can be implemented. But wait there is more!!!
Look how easy it is to setup a Factory to reproduce all of out Characters based off the saved setting.
CharacterFactory:Class
Function AddCharacters(Watcher As PropertySheetWatcher) As CharacterInterface
Dim Table As ListObject
Dim data As Variant
Dim RowIndex As Long
With Table
data = .DataBodyRange.Value
For RowIndex = 1 To UBound(data)
Select Case data(RowIndex, .ListColumns("Class").Index)
Case "Hero"
Set AddCharacters = AddCharacter(New Hero, Table, RowIndex)
Case "Turtle"
Set AddCharacters = AddCharacter(New Turtle, Table, RowIndex)
Case "Rhino"
Set AddCharacters = AddCharacter(New Rhino, Table, RowIndex)
Case "Wolf"
Set AddCharacters = AddCharacter(New Wolf, Table, RowIndex)
End Select
Next
End With
End Function
Private Function AddCharacter(Character As CharacterInterface, Table As ListObject, RowIndex As Long) As Object
With Character
.Speed = Table.ListColumns("Speed").DataBodyRange.Cells(RowIndex).Value
'....More Coe
End With
Set AddCharacter = Character
End Function
It may seem like I wrote a lot of original content but I didn't. The whole setup is an adaptation of concepts taken from different popular design patterns.

Properly coding a constructor

I am trying to implement a Model-View-Presenter Userinterface in VBA excel. In order to do this I have been writing different Model classes. Here an Example:
Option Explicit
Private Type TModel
FilterCol As Collection
N As Integer
End Type
Private this As TModel
Public Property Get FilterCol() As Collection
Set FilterCol = this.FilterCol
End Property
Public Property Let FilterCol(ByVal value As Collection)
Set this.FilterCol = value
End Property
Public Property Get N() As Integer
Set N = this.N
End Property
Public Property Let N(ByVal value As Integer)
Set this.N = value
End Property
This class called "FilterModel" is a collection of MSFormObjects. In order to use the collection properly I need to new it. So the code where I use it would look a little like this:
Sub testFilter()
Dim Filterm As FilterModel
Dim DefaultFilterLine As New FilterLine
Set Filterm = New FilterModel
Filterm.FilterCol = New Collection
'Set DefaultFilter
Filterm.FilterCol.Add DefaultFilterLine
'DoStuff
With New frmFilter
Set .Model = Filterm
.Show
End With
End Sub
If I don't new the Property FilterCol before I add something, in this case the defaultfilter, it doesn't work. So here is my Question:
Is there a way to overwrite the new statement for my new class in order to have it also new up the collection FilterCol. My research got me as far as I now know that this would be called a constructor.
But how would one properly implement a constructor for a VBA class?
Somthing like:
Private Sub Class_Initialize()
Set this.FilterCol = New Collection
N = 0
End Sub
If I do this then I get an error in the "Property Let N(Byval Value as integer)" Line. The error message reads "object required".
Here is a working solution. I suggest going through the code line-by-line using F8 to understand what is happening there. Debug.print prints values into the Immediate window.
Here is the FilterModel class:
''' FilterModel class
Option Explicit
Private pFilterCol As Collection
Private pN As Integer
Public Property Get FilterCol() As Collection
Set FilterCol = pFilterCol
End Property
Public Property Let FilterCol(ByVal value As Collection)
Set pFilterCol = value
End Property
Public Property Get N() As Integer
N = pN
End Property
Public Property Let N(ByVal value As Integer)
pN = value
End Property
Private Sub Class_Initialize()
Set pFilterCol = New Collection
pN = 0
End Sub
and here is module code to test it:
''' random module
Option Explicit
Sub testFilter()
Dim Filterm As FilterModel
Set Filterm = New FilterModel
Filterm.FilterCol = New Collection
''' default values (specified in Class_Initialize())
Debug.Print Filterm.N
Debug.Print Filterm.FilterCol.Count
''' set the values through Property Let
Filterm.FilterCol.Add "whatever"
Filterm.FilterCol.Add "whenever"
Filterm.N = 6
''' print the new values (through Property Get)
Debug.Print Filterm.N
Debug.Print Filterm.FilterCol.Count
Debug.Print Filterm.FilterCol(1)
Debug.Print Filterm.FilterCol(2)
End Sub

Trying to set up a custom object model using example, not working

I am trying to set up a custom object model using an example I found in an answered question here on stackoverflow.
VBA Classes - How to have a class hold additional classes
Here is the code I have created based on the answer.
Standard Module
Sub test()
Dim i As Long
Dim j As Long
'code to populate some objects
Dim AssemList As Collection
Dim Assem As cAssem
Dim SubAssemList As Collection
Dim SubAssem As cSubAssem
Set AssemList = New Collection
For i = 1 To 3
Set SubAssemList = New Collection
Set Assem = New cAssem
Assem.Description = "Assem " & i
For j = 1 To 3
Set SubAssem = New cSubAssem
SubAssem.Name = "SubAssem" & j
SubAssemList.Add SubAssem
Next j
Set Assem.SubAssemAdd = SubAssemList '<------ Object variable or With Block not Set
AssemList.Add Assem
Next i
Set SubAssemList = Nothing
'write the data backout again
For Each clock In AssemList
Debug.Print Assem.Description
Set SubAssemList = Assem.SubAssems
For Each SubAssem In SubAssemList
Debug.Print SubAssem.Name
Next
Next
End Sub
cAssem Class
Private pDescription As String
Private pSubAssemList As Collection
Private Sub Class_Initialize()
Set pSubAssems = New Collection
End Sub
Public Property Get Description() As String
Description = pDescription
End Property
Public Property Let Description(ByVal sDescription As String)
pDescription = sDescription
End Property
Public Property Get SubAssems() As Collection
Set SubAssems = pSubAssemList
End Property
Public Property Set SubAssemAdd(AssemCollection As Collection)
For Each AssemName In AssemCollection
pSubAssemList.Add AssemName ' <------- This is the line that is triggering the error
Next
End Property
cSubAssem Class
Private pSubAssemName As String
Public Property Get Name() As String
Name = pSubAssemName
End Property
Public Property Let Name(ByVal sName As String)
pSubAssemName = sName
End Property
I have not changed anything in the code except class names and variable names and from my limited point of view I cannot understand the cause of the error.
I am just starting to really dig into objects and Class Modules in VBA so I appreciate any knowledge this community could pass my way.
Many Thanks
You have a typo in your sub class initializer:
Private Sub Class_Initialize()
Set pSubAssems = New Collection
End Sub
should read:
Private Sub Class_Initialize()
Set pSubAssemList = New Collection
End Sub

Resources