Accessing a private field in class module - vba

I have this simplified class module clXXX....
':: backing field
Private fTemplateBk As Excel.Workbook
':::::::::::::::::::::::::::
'::
':: constructor
Private Sub Class_Initialize()
End Sub
':::::::::::::::::::::::::::
'::
':: properties
Property Get TemplateBk() 'As Excel.Workbook '<< different error messages depending on if "As Excel.Workbook" is included or not
TemplateBk = fTemplateBk
End Property
':::::::::::::::::::::::::::
'::
':: methods
Public Sub openTemplate()
Set fTemplateBk = Excel.Workbooks.Open("\\xxx\yyy\zzz.xlsx")
End Sub
Public Sub someMethod()
Me.TemplateBk.Sheets(1).Activate
End Sub
Normal module:
Sub control()
Dim x As clXXX
Set x = New clXXX
x.openTemplate
x.someMethod '<<<<<<errors here
End Sub
I want to only access the private field fTemplateBk via the read-only property TemplateBk using code such as me.TemplateBk. .... How do I amend the above so this is possible?

You need to make that property public and must use Set keyword.
Check the below code and confirm if it works:
Private fTemplateBk As Excel.Workbook
Public Property Get TemplateBk() As Excel.Workbook
Set TemplateBk = fTemplateBk
End Property

Your someMethod should use the private field fTemplateBk as it is private from outside, not from inside.
Public Sub someMethod()
Me.fTemplateBk.Sheets(1).Activate
End Sub
If you need to use the property from outside, in your normal module you should use:
Sub control()
Dim x As clXXX
Set x = New clXXX
x.openTemplate
x.TemplateBk.Sheets(1).Activate
End Sub
UPDATE 1
If you want to keep your code as it is, just add the SET in your TemplateBk getter and the code will work. As you can see in the picture, I used your same code with this change and it works.
Property Get TemplateBk() 'As Excel.Workbook
Set TemplateBk = fTemplateBk 'add the SET at the beggining
End Property

Related

How To Code So That Intellisense Follows The Complete Chain

While this code works and I can assign and retrieve values across all levels, intellisense only displays the methods or properties 1 level deep. How would I go about coding this so that I can follow my "Path" all the way down using intellisense and not necessarily have to just remember the methods or properties?
for instance if I type Wip. I get
but when I type Wip.Parts("Test"). , the SequenceNumbers member and its Methods/Properties are not displayed
I have the following code
clsSeq:
Option Explicit
Private iSeq As String
Private iQty As Double
Public Property Get Qty() As Double
Qty = iQty
End Property
Public Property Let Qty(lQty As Double)
iQty = lQty
End Property
Public Property Get Sequence() As String
Sequence = iSeq
End Property
Public Property Let Sequence(lSeq As String)
iSeq = lSeq
End Property
clsPart:
Option Explicit
Private iPart As String
Public SequenceNumbers As Collection
Public Property Get PartNumber() As String
PartNumber = iPart
End Property
Public Property Let PartNumber(lPart As String)
iPart = lPart
End Property
Public Sub AddSequence(aSeq As String, aQty As Double)
Dim iSeq As clsSeq
If SeqExists(aSeq) Then
Set iSeq = SequenceNumbers.Item(aSeq)
iSeq.Qty = iSeq.Qty + aQty
Else
Set iSeq = New clsSeq
With iSeq
.Sequence = aSeq
.Qty = aQty
End With
SequenceNumbers.Add iSeq, iSeq.Sequence
End If
Set iSeq = Nothing
End Sub
Private Sub Class_Initialize()
Set SequenceNumbers = New Collection
End Sub
Private Function SeqExists(iSeq As String) As Boolean
Dim v As Variant
On Error Resume Next
v = IsObject(SequenceNumbers.Item(iSeq))
SeqExists = Not IsEmpty(v)
End Function
clsParts:
Option Explicit
Public Parts As Collection
Public Sub AddPart(iPart As String)
Dim iPrt As clsPart
If Not PartExists(iPart) Then
Set iPrt = New clsPart
With iPrt
.PartNumber = iPart
End With
Parts.Add iPrt, iPrt.PartNumber
End If
End Sub
Private Function PartExists(iPT As String) As Boolean
Dim v As Variant
On Error Resume Next
v = IsObject(Parts.Item(iPT))
PartExists = Not IsEmpty(v)
End Function
Private Sub Class_Initialize()
Set Parts = New Collection
End Sub
modTest:
Sub TestWipCls()
Dim Wip As clsParts
Dim Part As clsPart
Set Wip = New clsParts
Wip.AddPart ("Test")
Set Part = Wip.Parts("Test")
Part.AddSequence "Proc7", 1505
Debug.Print Wip.Parts("Test").SequenceNumbers("Proc7").Qty
Part.AddSequence "Proc7", 100
Debug.Print Wip.Parts("Test").SequenceNumbers("Proc7").Qty
End Sub
That is because Parts is a Collection and its Default Member Call (or .Item) will return a value/object depending what was stored. While editing your code VBA does not know what kind of value/object is stored in the collection (as this is only established during run-time, eg. late-bound), so it can not give you any Intellisense-suggestions.
To circumvent this, you need a method (property/function) that returns a defined type of value/object (early-bound).
btw. (myCollection.("Foo") is the same as myCollection.Item("Foo"))
The solution is to create a custom collection that returns a value of a defined type.
The following example also explains how to implement a custom Collection so you can use the default member call instead of using .Item.
How to use the Implements in Excel VBA
While we're at it, please never use public variables in classes, make them accessible via Property Let/Set/Get methods!
More on this here: https://rubberduckvba.wordpress.com/2019/07/08/about-class-modules/
Edit:
Example for a custom Collection for classes that implement ICustomElement (Interfaces are explained in the link above)
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "CustomCollectionTemplate"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
'#Folder("Classes")
Option Explicit
Private Type TCustomCollection
CustomCollection as Collection
End Type
Dim this as TCustomCollection
Private Sub Class_Initialize()
Set this.CustomCollection = New Collection
End Sub
Private Sub Class_Terminate()
Set this.CustomCollection = Nothing
End Sub
Public Property Get NewEnum() As IUnknown
Attribute NewEnum.VB_UserMemId = -4
Attribute NewEnum.VB_MemberFlags = "40"
Set NewEnum = this.CustomCollection.[_NewEnum]
End Property
Public Sub Add(ByVal newCustomElement As ICustomElement)
this.CustomCollection.Add newCustomElement
End Sub
Public Sub Remove(ByVal Index As Long)
this.CustomCollection.Remove Index
End Sub
Public Function Item(ByVal Index As Long) As ICustomElement
Set Item = this.CustomCollection.Item(Index)
End Function
Public Function Count() As Long
Count = this.CustomCollection.Count
End Function
Thanks to M.Doerner & Mathieu Guindeon for the edits/comments

Is there a way to make a setable function in a user defined object

Is there a way to set an object method like a property?
I want to be able to just call the function like myObj.myFunc() but I want to set where myFunc will point when i instantiate myObj. As of right now i have it as a public event that i can add a handler to at init but it does seem like the best option.
As #VisualVincent said:
Module Module1
Sub Main()
Dim myObj As New myObj(Sub() Console.WriteLine("1"))
' OR
Dim myObj2 As New myObj(AddressOf myFunc)
myObj2.myFunc.Invoke
End Sub
Sub myFunc()
Console.WriteLine("2")
End Sub
End Module
Class myObj
Public Sub New(myFunc As Action)
Me.myFunc = myFunc
End Sub
Property myFunc As Action
End Class

Access a class modules variables within a second class

I was wondering if there's a way in which I can share variables between instances of separate class modules?
I have two classes:
Class 1
Class 2
Inside class 1, I have multiple global variables which I would like Class 2 to have access to once instantiated.
I could use get and set properties for each of the variables but I have about 40/50 so it just seems a bit tedious.
So, instead, I'm trying to pass the current instance of Class 1 to Class 2 using set property.
I've created a minimal example to illustrate my current efforts:
Class 1:
Public test As String
Private Sub Class_Initialize()
Call setTest
Dim b As Class2
Set b = New Class2
End Sub
Public Property Set Classed(ByRef vClass As Class1)
Set vClass = Me
End Property
Public Sub setTest(t As String)
test = "Sam"
End Sub
Class 2:
Private Sub Class_Initialize()
Dim newClass As Class1
newClass.Classed = newClass
' Want to be able to access the test String from class 1
End Sub
Obviously what I am doing at the moment is incorrect, so am wondering if someone could point out where I'm going wrong and show me how to achieve this class sharing?
Just to add: when running the code, I receive a compile error at line: newClass.Classed = newClass. Error: Invalid use of property
Not too sure but I sense a bit of a Circular Reference in your example?
What Are Circular References?
A circular reference occurs when two objects hold references to each other.
You could try an alternative by exposing a Dictionary object through your class, where the Key will be your "variable name", and the Value will hold the actual value.
An example could be:
Class1
Option Explicit
Private mList As Object
Public Property Get List() As Object
Set List = mList
End Property
Private Sub Class_Initialize()
Set mList = CreateObject("Scripting.Dictionary")
End Sub
Private Sub Class_Terminate()
Set mList = Nothing
End Sub
Implementation:
Sub ClassTest()
Dim a As Class1
Set a = New Class1
Dim b As Class1
Set b = New Class1
a.List("VarName") = "Sam" 'Set
b.List("VarName") = a.List("VarName") 'Get / Set
Debug.Print b.List("VarName") 'Get
Set a = Nothing
Set b = Nothing
End Sub
'Output
'Sam

Creating A Custom Event With VBA

I'm trying struggling to understand how to create a custom event using class modules in VBA.
I've put together the simple following example. You put a value in A1 and B1 and then re activate the sheet to calculate the sum of the two and then I hoped an event would fire to warn of calculation, but nothing happens.
I'd be very grateful for any help solving this example.
Class module cCalc:
Dim m_wks As Worksheet
Public Event BeforeCalc()
Property Set Worksheet(wks As Worksheet)
Set m_wks = wks
End Property
Public Sub Calc()
Dim dVal1 As Double
Dim dVal2 As Double
With m_wks
dVal1 = .Range("A1").Value
dVal2 = .Range("B1").Value
RaiseEvent BeforeCalc
.Range("C1").Value = dVal1 + dVal2
End With
End Sub
In a module mGlobal:
Public gCalc As cCalc
In the code behind Sheet1:
Private WithEvents calcEvent As cCalc
Private Sub calcEvent_BeforeCalc()
MsgBox "About to Calc!", vbInformation
End Sub
Private Sub Worksheet_Activate()
Set gCalc = New cCalc
Set gCalc.Worksheet = ActiveSheet
gCalc.Calc
End Sub
You can't declare event-driven classes in modules. You'll need to set the cCalc reference in gModule equal to the object you declared WithEvents in Sheet1. Change your code in Sheet1 to what i wrote below and it will work:
Private WithEvents calcEvent As cCalc
Private Sub calcEvent_BeforeCalc()
MsgBox "About to Calc!", vbInformation
End Sub
Private Sub Worksheet_Activate()
Set calcEvent = New cCalc 'Instantiate the WithEvents object above
Set mGlobal.gCalc = calcEvent 'Set the object declared in gModule
Set mGlobal.gCalc.Worksheet = ActiveSheet
mGlobal.gCalc.Calc
End Sub
Note that this is using the variable you put in gModule... The event that is actually called is still calcEvent_BeforeCalc(), which is good as this way you can have n number of objects defined in gModule that would all fire off the same event code when the event is triggered.
To simplify the code, you could always just write:
Private Sub Worksheet_Activate()
Set calcEvent = New cCalc
Set calcEvent.Worksheet = ActiveSheet
calcEvent.Calc
End Sub

Calling OnTime method in a Custom Class

I'm trying to use the .OnTime method in a class module, but can't figure out how to call a procedure in the class. All of the .OnTime examples I've seen refer to using the method from a standard code module rather than a custom class. Is there any way of calling a procedure in the class module rather than a standard code module?
#Alex P: Updated to include code. Here is the Class Module:
Option Explicit
Public Sub Test()
MsgBox "Success"
End Sub
Private Sub Class_Initialize()
Application.OnTime EarliestTime:=Now + TimeValue("00:00:03"), _
Procedure:="Test"
End Sub
And the Standard Module:
Option Explicit
Public Sub TestOnTime()
Dim OnTime As CCOnTime
Set OnTime = New CCOnTime
End Sub
I've also tried Procedure:="CClass.Test"
You can do it, but the call-back needs to be bounced back into the object from a Standard Module or a Worksheet Module or Thisworkbook.
Here is an example that pulses a value in a worksheet cell.
The timer is (almost) encapsulated in the cOnTime Class.
A cOnTime Object is instantiated in the host worksheet, whose code module can have a property to set the pulse time as well as the call-back routine.
If you protect the sheet, it will start pulsing and you can stop it by un-protecting the sheet.
If you navigate away from the host sheet, the timer is killed and if you navigate back it re-starts (as long as the sheet is protected).
Class cOnTime
Option Explicit
Const DEFPulseTime = "PulseTime"
Const DEFearliestTime As Long = 5
Const DEFlatestTime As Long = 15
Public WithEvents wb As Workbook
Public ws As Worksheet
Private DoWhen As String
Public mPulseTime As Long
Public mNextTime As Double
Property Let callBackDoWhen(cb As String)
DoWhen = "'" & wb.Name & "'!" & ws.CodeName & "." & cb 'e.g. 'wb Name.xlsm'!Sheet1.kickdog
End Property
Private Function PulseTime() As Long
On Error Resume Next
PulseTime = CallByName(ws, DEFPulseTime, VbGet)
If Err.number <> 0 Then
PulseTime = DEFearliestTime
End If
On Error GoTo 0
End Function
Property Get designMode() As Boolean
designMode = Not ws.ProtectContents
End Property
Public Sub kickDog()
Const myName As String = "kickDog"
Dim psMessage As String
If ws Is ActiveSheet And Not designMode Then
mNextTime = Now + TimeSerial(0, 0, mPulseTime)
On Error Resume Next
Application.OnTime mNextTime, DoWhen
On Error GoTo 0
End If
Exit Sub
End Sub
Public Sub killDog()
If ws Is Nothing Or mNextTime = 0 Then Exit Sub
On Error Resume Next
Application.OnTime mNextTime, DoWhen, , False
On Error GoTo 0
End Sub
Private Sub Class_Initialize()
Dim errorContext As String
On Error GoTo enableAndExit
Set wb = ActiveWorkbook
Set ws = ActiveSheet
On Error GoTo 0
callBackDoWhen = DEFDoWhen
callBackPulseTime = DEFPulseTime
mPulseTime = PulseTime
kickDog
Exit Sub
enableAndExit:
If Err <> 0 Then
If ws Is Nothing Then
errorContext = "ws"
ElseIf wb Is Nothing Then
errorContext = "wb"
End If
End If
End Sub
Private Sub Class_Terminate()
Const myName As String = "Class_Terminate"
On Error Resume Next
killDog
Set ws = Nothing
Set wb = Nothing
Exit Sub
End Sub
Private Sub wb_WindowActivate(ByVal Wn As Window)
wb_Open
End Sub
Private Sub wb_WindowDeactivate(ByVal Wn As Window)
killDog
End Sub
Private Sub wb_BeforeClose(Cancel As Boolean)
killDog
End Sub
Private Sub wb_BeforeSave(ByVal SaveAsUI As Boolean, Cancel As Boolean)
If SaveAsUI Then killDog
End Sub
In Worksheet Module
Option Explicit
Const cPulseTime As Long = 1
Dim mOnTime As cOnTime
Property Get PulseTime() As Long
PulseTime = cPulseTime
End Property
'****************************************
'Timer call-back for cOnTime
Public Sub kickDog()
' Code to execute on timer event
'******************************************
On Error Resume Next
Me.Cells(1, 1) = Not Me.Cells(1, 1)
On Error GoTo 0
'******************************************
Debug.Print "woof!!"
mOnTime.kickDog
End Sub
Private Sub Worksheet_Activate()
Me.Cells(1,1) = False
Set mOnTime = New cOnTime
End Sub
Private Sub Worksheet_Deactivate()
On Error Resume Next
Set mOnTime = Nothing
End Sub
You are asking for magic - VBA is a comprehensive tool but it is not magic.
The reason is that every Class module is simply a template which can be instantiated any number of times in the application code. Excel could not hope to correctly guess which particular instantiation of the Class module is the correct one on which to invoke the method. You are responsible for making this decision and managing the references to the appropriate Class instance.
Ah you say - But there is no private data/references being used by the method I want called. It is a static method. Well the answer to that is that VBA does not support static methods on Class modules, only on Standard modules. Any method that you wish to declare to the environment as being static is declared as being static by being included in a standard module.
So, place your call-back method in a Standard module, and declare a private member that holds a reference to the particular instance of the Class that you wish to handle the event.
One alternative route which isn't specified by others here but, I think, would theoretically work is:
Register VBA object as an active object with a specified GUID
Launch a PowerShell (or binary) daemon to connect to the object with specified GUID and call a specified method on that object every n seconds.
Pros:
Totally encapsulated
Cons:
Launches external process
Method likely has to be public (unless connection points can be abused)
Potentially crash-prone
I haven't implemented such a solution yet but intend to on stdCOM of stdVBA library if everything works out.