What I'm trying to do / the problem:
I'm effectively building a task list with a tree format; this would be made up of two class types Task_List and Task.
Task_Lists contain other Task_Lists or Tasks and many of their properties are the sum of the 'same' properties of the children that it contains. For example a property might be Duration, and so the Duration of a Task_List would be the sum of all the Durations of the Tasks and Task_Lists contained within it.
I've worked out how to do most of this (albeit after being stuck for quite awhile trying to find out how to access a parent class from a child one), but what I'm struggling with is an eloquent way to update the properties of a Task_List when the relevant property of one of its children is updated. The two methods I can get to work right now are both, in my opinion, flawed; for different reasons.
Option 1)
Method: Every time a value is changed for parameter X in a child, trigger a public function in the parent which resets X in the parent, then iterates through all the children and sums X; this is then set as the parent's value.
My issue with this: Whilst this works it feels messy and also perhaps 'wasteful' in that it has to loop through all children any time one of them is changed. If there's a lot of tasks and each of them is changed this could stack-up.
Option 2)
Method: Have the Task_List also have a Let property for X, and have the child call it to first remove its old value of X, and then add its new value of X.
My issue with this: Less 'wasteful' than Option 1, however this leaves the parent open to having its value for X changed by something other than the child. It would be nice to restrict it in some way so that it can only be called by a child.
Option 3)
Method: Basically the same as Option 2 but instead of passing a value through the function call, pass the child in its entirety. Then have the parent check that what has been passed is actually one of its children (or at least that the child reports that the parent is its parent; not perfect but I think that would be good enough and it's easier to implement imo).
My issue with this: I can't get it to work; and I have a feeling in VBA it might not be possible?
Any help, guidance, etc. would be greatly appreciated; and if any further information/clarification is required please just say and I'll provide it via edits/comments :)
Simplified example code:
Task class module:
Private Type Task
Name As String
Value As Long
Parent as Task_List
End Type
Private self As Task
Public Sub Initialize(Parent as Task_List)
Set self.Parent = Parent
End Sub
Public Property Get Parent() as Task_List
Set Parent = Self.Parent
End Property
Property Let Name(ByVal Name As String)
self.Name = Name
End Property
Property Get Name() As String
Name = self.Name
End Property
' I've put both options 2 & 3 here, but naturally only one would really be used at a time.
Property Let Value(ByVal Value As Long)
Call self.Parent.Option_2(-self.Value)
Call self.Parent.Option_3(self,-1)
self.Value = Value
Call self.Parent.Option_3(self,1)
Call self.Parent.Option_2(self.Value)
End Property
Property Get Value() As Long
Value = self.Value
End Property
Task list class:
Private Type Task_List
Name As String
SubTasks As New Collection
Value As Long
End Type
Private self As Task_List
Public Property Get SubTasks() As Collection
Set SubTasks = self.SubTasks
End Property
Public Property Let SubTasks(SubTask_Coll As Collection)
Set self.SubTasks = SubTask_Coll
End Property
Property Let Name(ByVal Name As String)
self.Name = Name
End Property
Property Get Name() As String
Name = self.Name
End Property
Property Get Value() As Long
Value = self.Value
End Property
Public Sub Option_2(ByVal Update_Value as long, _
Optional ByVal Multiplier as Long = 1)
self.Value = self.Value + (Multiplier * Update_Value)
End Sub
Public Sub Option_3(ByVal Child_Task as Variant, _
Optional ByVal Multiplier as Long = 1)
On Error Resume Next
If Child_Task.Parent = self Then
self.Value = self.Value + (Multiplier * Child_Task.Value)
End If
On Error GoTo 0
End Sub
Public Sub Option_1()
Dim Cur_Sum As Long, Cur_Task as Variant
For Each Cur_Task In self.SubTasks
Cur_Sum = Cur_Sum + Cur_Task.Value
Next Cur_Task
self.Value = Cur_Sum
End Sub
Example of them in use:
Sub Example()
Dim Main_List As New Task_List
Main_List.Name = "Bake Bread"
Dim T1 As New Task
T1.Initialize Main_List
Main_List.Subtasks.Add T1
T1.Name = "Buy Ingredients"
T1.Value = 250
Dim T2 As New Task
T2.Initialize Main_List
Main_List.Subtasks.Add T2
T2.Name = "Do Baking"
T2.Value = 400
Debug.Print Main_List.SubTasks(1).Name
Debug.Print Main_List.SubTasks(2).Name
Debug.Print Main_List.Value
End Sub
So I've found the issue was trying to pass "self" when those needed to be "Me"s (I also 'TIL'ed that On Error Resume Next causes errors in If statements to 'activate' them as if they were True).
So the Update_Value function in the Task_List class becomes:
Public Sub Update_Value(ByVal Child_Task as Variant, _
Optional ByVal Multiplier as Long = 1)
On Error GoTo Exit_Sub
If Child_Task.Parent Is Me Then
self.Value = self.Value + (Multiplier * Child_Task.Value)
End If
On Error GoTo 0
Exit_Sub:
End Sub
And the Let Value property of the Task class becomes:
Property Let Value(ByVal Value As Long)
Call self.Parent.Update_Value(Me, -1)
self.Value = Value
Call self.Parent.Update_Value(Me, 1)
End Property
(In the real code I have an Enum for Multiplier to restrict it to just -1 and +1).
Additionally, I've used this ability (of using Me) to modify the Initialize sub in the Task class to also add it to the parent whilst defining the parent. Thus:
Public Sub Initialize(Parent as Task_List)
Set self.Parent = Parent
Parent.SubTasks.Add Me
End Sub
Related
Preface
About 10 years ago I started refactoring and improving the ChartSeries class of John Walkenbach. Unfortunately it seems that the original it is not available any more online.
Following the Rubberduck Blog for quite some time now I try to improve my VBA skills. But in the past I only have written -- I guess the experts would call it -- "script-like god-procedures" (because of not knowing better). So I am pretty new to classes and especially interfaces and factories.
Actual Questions
I try to refactor the whole class by dividing it into multiple classes also using interfaces and than also adding unit tests. For just reading the parts of a formula it would be sufficient to get the Series.Formula and then do all the processing. So it would be nice to call the Run sub in the Create function. But everything I tried so far to do so failed. Thus, I currently running Run in all Get properties etc. (and test, if the formula changed and exit Run than. Is this possible and when yes, how?
Second, to add unit tests -- of course using rubberduck for them -- I currently rely on real Charts/ChartObjects. How do I create a stub/mock/fake for a Series? (Sorry, I don't know the correct term.)
And here a simplified version of the code.
Many thanks in advance for any help.
normal module
'#Folder("ChartSeries")
Option Explicit
Public Sub ExampleUsage()
Dim wks As Worksheet
Set wks = ThisWorkbook.Worksheets(1)
Dim crt As ChartObject
Set crt = wks.ChartObjects(1)
Dim srs As Series
Set srs = crt.Chart.SeriesCollection(3)
Dim MySeries As IChartSeries
Set MySeries = ChartSeries.Create(srs)
With MySeries
Debug.Print .XValues.FormulaPart
End With
End Sub
IChartSeries.cls
'#Folder("ChartSeries")
'#Interface
Option Explicit
Public Function IsSeriesAccessible() As Boolean
End Function
Public Property Get FullFormula() As String
End Property
Public Property Get XValues() As ISeriesPart
End Property
'more properties ...
ChartSeries.cls
'#PredeclaredId
'#Exposed
'#Folder("ChartSeries")
Option Explicit
Implements IChartSeries
Private Type TChartSeries
Series As Series
FullSeriesFormula As String
OldFullSeriesFormula As String
IsSeriesAccessible As Boolean
SeriesParts(eElement.[_First] To eElement.[_Last]) As ISeriesPart
End Type
Private This As TChartSeries
Public Function Create(ByVal Value As Series) As IChartSeries
'NOTE: I would like to run the 'Run' sub somewhere here (if possible)
With New ChartSeries
.Series = Value
Set Create = .Self
End With
End Function
Public Property Get Self() As IChartSeries
Set Self = Me
End Property
Friend Property Let Series(ByVal Value As Series)
Set This.Series = Value
End Property
Private Function IChartSeries_IsSeriesAccessible() As Boolean
Call Run
IChartSeries_IsSeriesAccessible = This.IsSeriesAccessible
End Function
Private Property Get IChartSeries_FullFormula() As String
Call Run
IChartSeries_FullFormula = This.FullSeriesFormula
End Property
Private Property Get IChartSeries_XValues() As ISeriesPart
Call Run
Set IChartSeries_XValues = This.SeriesParts(eElement.eXValues)
End Property
'more properties ...
Private Sub Class_Initialize()
With This
Dim Element As eElement
For Element = eElement.[_First] To eElement.[_Last]
Set .SeriesParts(Element) = New SeriesPart
Next
End With
End Sub
Private Sub Class_Terminate()
With This
Dim Element As LongPtr
For Element = eElement.[_First] To eElement.[_Last]
Set .SeriesParts(Element) = Nothing
Next
End With
End Sub
Private Sub Run()
If Not GetFullSeriesFormula Then Exit Sub
If Not HasFormulaChanged Then Exit Sub
Call GetSeriesFormulaParts
End Sub
'(simplified version)
Private Function GetFullSeriesFormula() As Boolean
GetFullSeriesFormula = False
With This
'---
'dummy to make it work
.FullSeriesFormula = _
"=SERIES(Tabelle1!$B$2,Tabelle1!$A$3:$A$5,Tabelle1!$B$3:$B$5,1)"
'---
.OldFullSeriesFormula = .FullSeriesFormula
.FullSeriesFormula = .Series.Formula
End With
GetFullSeriesFormula = True
End Function
Private Function HasFormulaChanged() As Boolean
With This
HasFormulaChanged = (.OldFullSeriesFormula <> .FullSeriesFormula)
End With
End Function
Private Sub GetSeriesFormulaParts()
Dim MySeries As ISeriesFormulaParts
'(simplified version without check for Bubble Chart)
Set MySeries = SeriesFormulaParts.Create( _
This.FullSeriesFormula, _
False _
)
With MySeries
Dim Element As eElement
For Element = eElement.[_First] To eElement.[_Last] - 1
This.SeriesParts(Element).FormulaPart = _
.PartSeriesFormula(Element)
Next
'---
'dummy which normally would be retrieved
'by 'MySeries.PartSeriesFormula(eElement.eXValues)'
This.SeriesParts(eElement.eXValues).FormulaPart = _
"Tabelle1!$A$3:$A$5"
'---
End With
Set MySeries = Nothing
End Sub
'more subs and functions ...
ISeriesPart.cls
'#Folder("ChartSeries")
'#Interface
Option Explicit
Public Enum eEntryType
eNotSet = -1
[_First] = 0
eInaccessible = eEntryType.[_First]
eEmpty
eInteger
eString
eArray
eRange
[_Last] = eEntryType.eRange
End Enum
Public Property Get FormulaPart() As String
End Property
Public Property Let FormulaPart(ByVal Value As String)
End Property
Public Property Get EntryType() As eEntryType
End Property
Public Property Get Range() As Range
End Property
'more properties ...
SeriesPart.cls
'#PredeclaredId
'#Folder("ChartSeries")
'#ModuleDescription("A class to handle each part of the 'Series' string.")
Option Explicit
Implements ISeriesPart
Private Type TSeriesPart
FormulaPart As String
EntryType As eEntryType
Range As Range
RangeString As String
RangeSheet As String
RangeBook As String
RangePath As String
End Type
Private This As TSeriesPart
Private Property Get ISeriesPart_FormulaPart() As String
ISeriesPart_FormulaPart = This.FormulaPart
End Property
Private Property Let ISeriesPart_FormulaPart(ByVal Value As String)
This.FormulaPart = Value
Call Run
End Property
Private Property Get ISeriesPart_EntryType() As eEntryType
ISeriesPart_EntryType = This.EntryType
End Property
Private Property Get ISeriesPart_Range() As Range
With This
If .EntryType = eEntryType.eRange Then
Set ISeriesPart_Range = .Range
Else
' Call RaiseError
End If
End With
End Property
Private Property Set ISeriesPart_Range(ByVal Value As Range)
Set This.Range = Value
End Property
'more properties ...
Private Sub Class_Initialize()
This.EntryType = eEntryType.eNotSet
End Sub
Private Sub Run()
'- set 'EntryType'
'- If it is a range then find the range parts ...
End Sub
'a lot more subs and functions ...
ISeriesParts.cls
'#Folder("ChartSeries")
'#Interface
Option Explicit
Public Enum eElement
[_First] = 1
eName = eElement.[_First]
eXValues
eYValues
ePlotOrder
eBubbleSizes
[_Last] = eElement.eBubbleSizes
End Enum
'#Description("fill me")
Public Property Get PartSeriesFormula(ByVal Element As eElement) As String
End Property
SeriesFormulaParts.cls
'#PredeclaredId
'#Exposed
'#Folder("ChartSeries")
Option Explicit
Implements ISeriesFormulaParts
Private Type TSeriesFormulaParts
FullSeriesFormula As String
IsSeriesInBubbleChart As Boolean
WasRunCalled As Boolean
SeriesFormula As String
RemainingFormulaPart(eElement.[_First] To eElement.[_Last]) As String
PartSeriesFormula(eElement.[_First] To eElement.[_Last]) As String
End Type
Private This As TSeriesFormulaParts
Public Function Create( _
ByVal FullSeriesFormula As String, _
ByVal IsSeriesInBubbleChart As Boolean _
) As ISeriesFormulaParts
'NOTE: I would like to run the 'Run' sub somewhere here (if possible)
With New SeriesFormulaParts
.FullSeriesFormula = FullSeriesFormula
.IsSeriesInBubbleChart = IsSeriesInBubbleChart
Set Create = .Self
End With
End Function
Public Property Get Self() As ISeriesFormulaParts
Set Self = Me
End Property
'#Description("Set the full series formula ('ChartSeries')")
Public Property Let FullSeriesFormula(ByVal Value As String)
This.FullSeriesFormula = Value
End Property
Public Property Let IsSeriesInBubbleChart(ByVal Value As Boolean)
This.IsSeriesInBubbleChart = Value
End Property
Private Property Get ISeriesFormulaParts_PartSeriesFormula(ByVal Element As eElement) As String
'NOTE: Instead of running 'Run' here, it would be better to run it in 'Create'
Call Run
ISeriesFormulaParts_PartSeriesFormula = This.PartSeriesFormula(Element)
End Property
'(replaced with a dummy)
Private Sub Run()
If This.WasRunCalled Then Exit Sub
'extract stuff from
This.WasRunCalled = True
End Sub
'a lot more subs and functions ...
You can already!
Public Function Create(ByVal Value As Series) As IChartSeries
With New ChartSeries <~ With block variable has access to members of the ChartSeries class
.Series = Value
Set Create = .Self
End With
End Function
...only, like the .Series and .Self properties, it has to be a Public member of the ChartSeries interface/class (the line is blurry in VBA, since every class has a default interface / is also an interface).
Idiomatic Object Assignment
A note about this property:
Friend Property Let Series(ByVal Value As Series)
Set This.Series = Value
End Property
Using a Property Let member to Set an object reference will work - but it isn't idiomatic VBA code anymore, as you can see in the .Create function:
.Series = Value
If we read this line without knowing about the nature of the property, this looks like any other value assignment. Only problem is, we're not assigning a value, but a reference - and reference assignments in VBA are normally made using a Set keyword. If we change the Let for a Set in the Series property definition, we would have to do this:
Set .Series = Value
And that would look much more readily like the reference assignment it is! Without it, there appears to be implicit let-coercion happening, and that makes it ambiguous code: VBA requires a Set keyword for reference assignments, because any given object can have a paraterless default property (e.g. how foo = Range("A1") implicitly assigns foo to the Value of the Range).
Caching & Responsibilities
Now, back to the Run method - if it's made Public on the ChartSeries class, but not exposed on the implemented IChartSeries interface, then it's a member that can only be invoked from 1) the ChartSeries default instance, or 2) any object variable that has a ChartSeries declared type. And since our "client code" is working off IChartSeries, we can guard against 1 and shrug off 2.
Note that the Call keyword is superfluous, and the Run method is really just pulling metadata from the encapsulated Series object, and caching it at instance level - I'd give it a name that sounds more like "refresh cached properties" than "run something".
Your hunch is a good one: Property Get should be a simple return function, without any side-effects. Invoking a method that scans an object and resets instance state in a Property Get accessor makes it side-effecting, which is a design smell - in theory.
If Run is invoked immediately after creation before the Create function returns the instance, then this Run method boils down to "parse the series and cache some metadata I'll reuse later", and there's nothing wrong with that: invoke it from Create, and remove it from the Property Get accessors.
The result is an object whose state is read-only and more robustly defined; the counterpart of that is that you now have an object whose state might be out of sync with the actual Excel Series object on the worksheet: if code (or the user) tweaks the Series object after the IChartSeries is initialized, the object and its state is stale.
One solution is to go out of your way to identify when a series is stale and make sure you keep the cache up-to-date.
Another solution would be to remove the problem altogether by no longer caching the state - that would mean one of two things:
Generating the object graph once on creation, effectively moving the caching responsibility to the caller: calling code gets a read-only "snapshot" to work with.
Generating a new object graph out of the series metadata, every time the calling code needs it: effectively, it moves the caching responsibility to the caller, which isn't a bad idea at all.
Making things read-only removes a lot of complexity! I'd go with the first option.
Overall, the code appears nice & clean (although it's unclear how much was scrubbed for this post) and you appear to have understood the factory method pattern leveraging the default instance and exposing a façade interface - kudos! The naming is overall pretty good (although "Run" sticks out IMO), and the objects look like they each have a clear, defined purpose. Good job!
Unit Testing
I currently rely on real Charts/ChartObjects. How do I create a stub/mock/fake for a Series? (Sorry, I don't know the correct term.)
Currently, you can't. When if/when this PR gets merged, you'll be able to mock Excel's interfaces (and much, much more) and write tests against your classes that inject a mock Excel.Series object that you can configure for your tests' purposes... but until then, this is where the wall is.
In the mean time, the best you can do is wrap it with your own interface, and stub it. In other words, wherever there's a seam between your code and Excel's object model, we slip an interface between the two: instead of taking in a Excel.Series object, you'd be taking in some ISeriesWrapper, and then the real code would be using an ExcelSeriesWrapper that works off an Excel.Series, and the test code might be using a StubSeriesWrapper whose properties return either hard-coded values, or values configured by tests: the code that works at the seam between the Excel library and your project, can't be tested - and we woulnd't want to anyway, because then we'd be testing Excel, not our own code.
You can see this in action in the example code for the next upcoming RD News article here; that article will discuss exactly this, using ADODB connections. The principle is the same: none of the 94 unit tests in that project ever open any actual connection, and yet with dependency injection and wrapper interfaces we're able to test every single bit of functionality, from opening a database connection to committing a transaction... without ever hitting an actual database.
is there way how to make class working similar to Arrays?
Let's say, I have Class (e.g. Workers) where main property is array of the Workers, nothing else.
Then I'm filling the class as follows
Dim wks as new Workers
wks.add("Worker1")
wks.add("Worker2")
wks.add("Worker3")
Then in Workers Class module:
Private Workers as Variant
Public Function add(ByVal val As Variant) As Long
ReDim Preserve Workers(LBound(Workers) To UBound(Workers) + 1)
Workers(UBound(Workers)) = val
add = UBound(Workers) - LBound(Workers) +1
End Function
Workers representation -> {"Worker1", "Worker2", "Worker3"}
Then I want to access Worker by its index. I know, how to access it by e.g wks.getWorker(1) but what I want to do, is to access it directly by wks(1) which should return "Worker 1". Example above looks, that usual Array or Collection can be used, but I have many internal methods done, only what I'm missing is to access Workers property to read/write directly by its index number.
Is it possible?
Edit
After transfer to Collections, Class looks like:
Option Explicit
Private Workers As Collection
Private Sub Class_Initialize()
Set Workers = New Collection
End Sub
Public Function add(ByVal val As Variant) As Long
Workers.add val
End Function
Public Property Get Item(Index As Integer) As Variant
Item = Workers(Index)
End Property
Public Property Set Item(Index As Integer, Value As Variant)
Workers.Remove Index
Workers.add Value, Before:=Index
End Property
with hidden attributes Attribute Item.VB_UserMemId = 0 at Getter and Setter.
Getting works fine:
Dim wks As New Workers
wks.add "Worker1"
wks.add "Worker2"
wks.add "Worker3"
Debug.Print wks(2) ' <-- OK here
'wks(2) = "Second Worker" ' <-- By debugging this go to Getter not Setter and after Getter is done, it allerts with Runtime error '424': Object required
Set wks(2) = "Second Worker" ' <-- This alert immediately Compile error: Object required on "Second Worker" string
Debug.Print wks(2)
Prints "Worker2" into console, thanks for this, but still I'm not able to set a new value to the required Index of the Workers Collection.
You could use a default member in VBA. Though you can't make the default memeber directly through VBA editor, but you can use any text editor.
Export your class from VBA editor, i.e. File->Export File
Open your exported class in Notepad (or any text editor)
Add this attribute line on your method or property you want to make it default. Attribute Item.VB_UserMemId = 0
You can for example make getWorker default member as.
Public Function GetWorker(Index As Integer) As Worker
Attribute Item.VB_UserMemId = 0
GetWorker = Workers(Index)
End Function
you can then use it like.
Set wk = wks(1)
Here is some detail about default members
http://www.cpearson.com/excel/DefaultMember.aspx
Edits
An example to make Getter/Setter as default member
Public Property Get Item(Index as Integer) as Worker
Attribute Item.VB_UserMemId = 0
Set Item = Workers(Index)
End Property
Public Property Set Item(Index as Integer, Value as Worker)
Attribute Item.VB_UserMemId = 0
Set Workers(Index) = Value
End Property
I'm having a class module with some data:
Private sharedFolders() As String
Public Property Let SetSharedFolders(val As String)
Dim i As Integer
sharedFolders = Array("folder one", "folder two")
i = UBound(sharedFolders)
i = UBound(sharedFolders)
ReDim Preserve sharedFolders(i)
sharedFolders(i) = CStr(val)
End Property
Property Get GetSharedFolders()
GetSharedFolders = sharedFolders()
End Property
And I want to add something to this property from other module like this:
Sub PrepareData()
Dim e
Dim s
Dim a(2) As String
Set e = New Entry
a(0) = "add one"
a(1) = "add two"
For Each s In a
e.SetSharedFolders (s) 'Here comes exception
Next
For Each s In e.GetSharedFolders
Debug.Print s
Next
End Sub
But I receive an "wrong number of arguments or invalid property assignment vba" exception... Can anyone assist?
Addendum
Thanks to #AJD and #Freeflow to pointing out a mistake and idea to make it easier. Decided to make as like below.
Class Module:
Private sharedFolders As New Collection
Public Property Let SetSharedFolders(val As String)
If sharedFolders.Count = 0 Then ' if empty fill with some preset data and add new item
sharedFolders.Add "folder 1"
sharedFolders.Add "folder 2"
sharedFolders.Add CStr(val)
Else
sharedFolders.Add CStr(val)
End If
End Property
Property Get GetSharedFolders() As Collection
Set GetSharedFolders = sharedFolders
End Property
and regular module:
Sub AddData()
Dim e As New Entry ' creating an instance of a class
Dim s As Variant ' variable to loop through collection
Dim a(1) As String 'some array with data to insert
a(0) = "add one"
a(1) = "add two"
For Each s In a
e.SetSharedFolders = s
Next
For Each s In e.GetSharedFolders
Debug.Print s
Next
End Sub
Initially I thought the problem lies in this code:
i = UBound(sharedFolders)
i = UBound(sharedFolders)
ReDim Preserve sharedFolders(i)
sharedFolders(i) = CStr(val)
i is set twice to the same value, and then the sharedFolders is reDimmed to the same value it was before! Also, there is some trickery happening with the use of ix within a 0-based array.
But the problem is most likely how you have declared your variables.
For Each s In a
e.SetSharedFolders (s) 'Here comes exception
Next
s is a Variant, and a is a Variant. At this point VBA is trying to guess how to handle a For Each loop with two Variants. And then the improper call is made. The correct syntax is:
e.SetSharedFolders s '<-- no parenthesis
There are plenty of posts on StackOverflow explaining how to call routines and what the impact of the evaluating parenthesis are!
However, at this point we are only assuming it is passing in a single element of the array - it could be passing the full array itself (albeit unlikely).
And the third factor -
Public Property Let SetSharedFolders(val As String)
The parameter val is being passed ByRef and should be passed ByVal. This also has unintended side effects as I found out (Type mismatch trying to set data in an object in a collection).
Public Property Let SetSharedFolders(ByVal val As String)
All in all you have the perfect storm of ambiguity driving to an unknown result.
The answer here is to strongly type your variables. This removes about two layers of ambiguity and areas where errors can happen. In addition, this will slightly improve code execution.
Another aspect is to understand when you should pass something ByVal and when to use the default (preferably explicitly) ByRef.
And a final gratuitous hint: Use a Collection instead of an Array. Your code you have implies a Collection will be more efficient and easier to manage.
Addendum
(thanks to #FreeFlow):
If the OP changes the definition of sharedfolders to Variant rather than String() then the array statement will work as expected.
The line e.SetSharedFolders (s) will work fine if it is changed to e.SetSharedFolders = s because the method SetSharedFolders is a Let Property not a Sub. There are other errors but these two changes will make the code run.
I have a model simluation coded in Excel VBA. It is built inside of a class module named "ChemicalRelease". There is another Class module named "UniversalSolver" which works to optimize parameters of the ChemicalRelease.
While running different simulations, universalSolver will sometimes use a combination of parameters that goes outside of the modeling bounds of the application. It is difficult to determine the true modeling boundaries as it is based on multiple combinations of parameters.
An instance of UniversalSolver will create a set of input parameters and instantiate ChemicalRelease to run a model as specified. Inside of ChemicalRelease, the flow works within several methods such as "setden" which may call other methods to perform their calculation. For example, "setden" may call "tprop" to determine thermodynamic properties, and "tprop" may in turn call a function to iteratively solve for a value.
At any point within any of these methods, the model may determine that the combination of input parameters cannot be solved. The current configuration notifies me of the issue thru a msgbox and stops the program, bringing it into debug mode.
I would like to make use of an event handler that will set a value of an instance of a handler that will stop calculations within "ChemicalRelease", set the instance to "Nothing" and return control to "UniversalSolver", directly after the line where "ChemicalRelease" was instantiated and called for modeling.
serveral google searches, and none point to a way to return control to "UniversalSolver".
'event handler code: credit to Change in variable triggers an event
"ClassWithEvent" class
Public Event VariableChange(value As Integer)
Private p_int As Integer
Public Property Get value() As Integer
value = p_int
End Property
Public Property Let value(value As Integer)
If p_int <> value Then RaiseEvent VariableChange(value) 'Only raise on
actual change.
p_int = value
End Property
"ClassHandlesEvent" class
Private WithEvents SomeVar As ClassWithEvent
Private Sub SomeVar_VariableChange(value As Integer) 'This is the event
handler.
'line here to return control to "UniversalSolver" instance, out of
"ChemicalRelease" instance, regardless of how many methods have to be
returned out of within ChemicalRelease.
End Sub
Public Property Get EventVariable() As ClassWithEvent
Set EventVariable = SomeVar
End Property
Public Property Let EventVariable(value As ClassWithEvent)
Set SomeVar = value
End Property
"Globals" Module
'Globally set instances for ClassHandlesEvent and ClassWithEvent
Global VAR As ClassHandlesEvent
Global TST As ClassWithEvent
"UniversalSolver" class
Public Sub initialize()
Set VAR = New ClassHandlesEvent
Set TST = New ClassWithEvent
VAR.EventVariable = TST
End Sub
Public Sub solve()
Do 'iterate through potential input parameters
Set m_chemRelease = New ChemicalRelease
m_chemRelease.initialize 'initializes and launches modeling
Loop until satisfied
End Sub
"ChemicalRelease" class
Public Sub initialize(modelParamsSheet As Worksheet)
Set m_modelParamsSheet = modelParamsSheet
Call readModelInputsAndSetProperties(0)
End Sub
Private Sub readModelInputsAndSetProperties(inNum As Integer)
'set all properties and launch modeling
Call setjet(0)
End Sub
Private Sub setjet(inInt As Integer)
'lots of math.
call tprop(tpropsInputDict)
'lots more math.
End Sub
Private Sub tprop(inDict as Scripting.Dictionary)
'more math.
'check for convergence
If check > 0.00001 Then
'failed convergence
'trigger event to exit ChemicalRelease Instance and return control
to UniversalSolver instance
TST.value = 2
End If
'more math.
Call limit()
End Sub
Private Sub limit()
'more math.
'check for sign
If fa * fb > 1 Then
'failed convergence
'trigger event to exit ChemicalRelease Instance and return control
to UniversalSolver instance
TST.value = 2
End If
'more math.
End Sub
Expected results are to have an event which can be triggered at any location within the project that will return control to UniversalSolver as if I was stating "exit sub" from within ChemicalRelease.initialize. However, I cannot find a valid method for this.
Error handling in the calling function works for all called functions. However, the "resume" command is required to take VBA out of error-handling mode. Per the code below, flow is returned to normal mode at the "endoffor" label in the calling function.
errcatch:
Err.Clear
On Error GoTo errcatch
Resume endoffor '
Let's say that I have an object...
Class test
Public a
Public b
End class
And in my code I would like to instantiate it without knowing a predetermined variable name to store the new instance.
Is this possible? How would I then dim the random variable?
I want to be able to do this because I don't want my user input to be stored or saved in the same variable with other data conflict. Like say for example I am storing stats for an athlete and I ask the athlete's name. If every time the user enters a different name, I don't want to have this information in the same object instance. Could I create an object for the athlete and reference this object inside another object?
I have a snippet of code:
Function addStats
dim pAtt, pComp, pInt, pTds, pYds, endNum, pName
Wscript.StdOut.WriteLine "What is your quarterback's name"
pName = Wscript.StdIn.ReadLine
Wscript.StdOut.WriteLine "How many attempts: "
pAtt = Wscript.StdIn.ReadLine
'chkNum(pAtt)
Wscript.StdOut.WriteLine "How many completions: "
pComp = Wscript.StdIn.ReadLine
'chkNum(pComp)
Wscript.StdOut.WriteLine "How many yards: "
pYds = Wscript.StdIn.ReadLine
'chkNum = pYds
Wscript.StdOut.WriteLine "How many touchdowns: "
pTds = Wscript.StdIn.ReadLine
'chkNum = pTds
Wscript.StdOut.WriteLine "How many interceptions: "
pInt = Wscript.StdIn.ReadLine
'chkNum = pInt
endNum = UBound(newStats) + 1
redim preserve newStats(endNum)
'---- vvvv ----
set newStats(endNum) = new QB
'---- ^^^^ ----
newStats(endNum).att = pAtt
newStats(endNum).comp = pComp
newStats(endNum).yds = pYds
newStats(endNum).tds = pTds
newStats(endNum).ints = pInt
newStats(endNum).qbname = pName
Wscript.StdOut.WriteLine "Stats Added"
writeBuffer()
end Function
The object is:
class QB
dim att, comp, yds, tds, ints, qbname
public property let qbAtt(n)
att = n
end property
public property let qbComp(n)
comp = n
end property
public property let qbYds(n)
yds = n
end property
public property let qbTds(n)
tds = n
end property
public property let qbInt(n)
ints = n
end property
public property let qName(n)
qbname = n
end property
public property get qbAtt
qbAtt = att
end property
public property get qbComp
qbComp = comp
end property
public property get qbYds
qbYds = yds
end property
public property get qbTds
qbTds = tds
end property
public property get qbInt
qbInt = ints
end property
public property get qName
qName = qbname
end property
end class
The highlighted statement is me instantiating the object with a global variable - newStat(). My belief is that I would have to either create a class for just the quarterback and somehow reference this into a variable array that is determined on the quarterback's name or create a function that takes the user prompt variable that asks the quarterback's name and set it as an array that instantiates the QB class.
I think you're confusing classes, objects, and variables. Classes are basically templates that describe which properties and behavior entities of a particular category have. Objects are instances of a class. Variables are identifiers that refer to objects (or data of primitive types).
Take lockers for example. Lockers usually have a color, they can be opened and closed, and they may contain a number of items. Thus a general description of lockers (a class "Locker") might look like this:
Class Locker
Public color
Public closed
Public content
Sub Class_Initialize
closed = True
content = Array()
End Sub
Sub Open
closed = False
End Sub
Sub Close
closed = True
End Sub
End Class
The constructor (Class_Initialize) is a special method that is only called when an object (an instance of a class) is created. It sets the initial state of the object. Kind of like when a locker is built in the factory.
A particular locker (object) might be green and contain a hat and a magazine, while another locker (object) might be blue and contain a book and a jacket.
Set locker_A = New Locker
locker_A.color = "green"
locker_A.content = Array("hat", "magazine")
Set locker_B = New Locker
locker_B.color = "blue"
locker_B.content = Array("book", "jacket")
To be able to actually work with objects (or other data) in a program you need variables. These identifiers (locker_A and locker_B in the example above) allow you to refer to particular objects in your program code in order to access their properties and methods.
When you run a statement
Set newStats(2) = New QB
it creates a new instance of the class QB (a new object) and places a reference to that object in the third slot of the array newStats. Afterwards you can use newStats(2) to refer to that object in your program or script. For instance:
newStats(2).yds = 42 'yards
If you have a number of QB object and store each of them in a different array slot there shouldn't be any conflict between them, as long as you don't replace a reference in one slot with another one.
Set newStats(2) = newStats(5) '<-- don't do this
If you wanted to access objects from the list by a particular property (for instance the name) instead of by index you'd use a Dictionary instead of an array:
Set newStats = CreateObject("Scripting.Dictionary")
newStats.CompareMode = vbTextCompare 'make lookups case-insensitive
...
Set player = New QB
player.att = pAtt
...
player.qbname = pName
newStats.Add pName, player
Then you could access a particular player like this:
name = "Charley Johnson"
WScript.Echo newStats(name).yds