Malfunction of the scope of a module public variable? - excel

In this simple code the scope of two Public variables of General module (gsOT and gsPath) works fine.
If UF_Printer.CB_Ok_Click () form event call General.CheckPath function, variables are visible and has same value in both scopes, form and module, as you can see in this screenshot of the inspection window
Public gsOT As String
Public gsPath As String
Public Function CheckPath(ByVal sPath_I As String) As String
gsOT = "1234"
gsPath = sPath_I & "\"
CheckPath = gsPath
End Function
'Ok command button in UF_Printer user form
Private Sub CB_Ok_Click()
Dim PrintPath As String
PrintPath = CheckPath("C:\PDM\Client")
PrintPath = gsPath & gsOT
End Sub
But in a real environment, two variables (gsOT and gsRuta) also declared Public in General module, do not work the same when called from Formulario_Printer.CB_Ok_click() form event.
gsOT variable works correctly and has a value in Formulario_Printer form, but gsRuta is empty.
And both are out of General module context when the application focus is on the form context. However, in the simple example they are in context and have the same value.
The code of the real environment is very complex to put it but the operation is the same as in the simple example: from a form I call a function of a module to check and fill some variables. I have taken some screenshots in case this can help.
Before calling the function
While working on the function
After calling the function

Related

Can't see created objects

I've created some simple classes in excel and I'm trying to create new objects from these classes. It works fine and let's me create them and I can also access the variables given to the object. I can't see the object in the local window though and I don't really understand why. Is it not created correctly because you are supposed to see your objects there I understand?
Here is the code for the class
Option Explicit
'Teams
Public Name As String
Public Group As String
Public GF As Integer
Public GA As Integer
Public Points As Integer
'Public Players(25) As String
Private Sub class_initialize()
Points = 5
End Sub
and here is the code where I try to create an object
Sub TestTeams()
Dim Madagaskar As Object
Set Madagaskar = New ETeam
MsgBox (Madagaskar.Points)
End Sub
If you put Stop on the line after the MsgBox call and run TestTeams, you will see the object in the locals window.
It will only be there while Madagaskar is in scope and you're in break mode.

How to run a sub in a `Create` function and how to make a mock/stub/fake for a chart `Series`?

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.

Excel VBA - Return Control to Calling Method when the called method has already called another method

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 '

Defining cross-module variables with values

I'm developing a bunch of Excel Macros for making my life easier. One part of different macros is inserting a picture into sheets. For this reason, I would like to save the path to the images in a global location and then access it via a variable (so that I don't have to manually adjust the paths in every macro if it changes). I use one module per macro
In my own module "Variables" I defined a variable as Public or Global and then assigned a value via a sub. If I now access this variable via another module, I get an empty MsgBox.
For test purposes I use a string which I want to display via an MsgBox.
Modul 1:
Public test As String
Sub variablen()
test = "String for Test "
End Sub
Modul 2:
Public Sub testpublic()
MsgBox (test)
End Sub
I recommend to use a constant instead of a variable:
Module 1
Option Explicit
Public Const MyPath As String = "C:\Temp"
Module 2
Option Explicit
Public Sub ShowPath()
MsgBox MyPath
End Sub
I also recommend to activate Option Explicit: In the VBA editor go to Tools › Options › Require Variable Declaration.
If you do it like you did test is empty until it was initialized by running the procedure variablen first. If you use Public Const no initialization is required.
so that I don't have to manually adjust the paths in every macro if it changes
If it ever needs to change, then it semantically isn't a Const. The key to writing code that you don't constantly need to modify is to separate the code from the data.
A file path that sometimes needs to change can be seen as some kind of configuration setting.
Have a module that is able to read the settings from wherever they are, and return the value of a setting given some key.
The settings themselves can live on a (hidden?) worksheet, in a ListObject table with Key and Value columns, and looked up with INDEX+MATCH functions (using the early-bound WorksheetFunction functions will throw run-time errors given a non-existing key string):
Option Explicit
Public Function GetSettingValue(ByVal key As String) As Variant
With SettingsSheet.ListObjects(1)
GetSettingValue = Application.WorksheetFunction.Index( _
.ListColumns("Value").DataBodyRange, _
Application.WorksheetFunction.Match(key, .ListColumns("Key").DataBodyRange, 0))
End With
End Function
The Variant will retain the subtype of the Value, so for a String value you get a Variant/String; for a Date value you get a Variant/Date, for a numeric value you get a Variant/Double, and for a TRUE/FALSE value you get a Variant/Boolean.
Now when the file path needs to change, your code does not:
Dim path As String
path = GetSettingValue("ImageFolderPath")
And if you need more settings, you have no code to add, either:
Dim otherThing As String
otherThing = GetSettingValue("OtherThing")
All you need to do is to make sure the string keys being used match the contents of the Key column in your SettingsSheet.

Excel VBA - Use an existing string in called sub

I'm pretty new to this so apologies in advance
I'm half way through a userform in Excel and I'm trying to cut some fat off my code by using Call - I have 12 buttons that all do the same thing, the only difference is that each buttons sub is dependant on the buttons caption. My problem is that I can't figure out a way to use a String I've already declared in the Buttons Sub, then use it in the called Sub. I know you can do it, but my googling skills have failed me :(
Please could someone show me how to do this? Hope that all makes sense...
Here is a very small snippet of my code, but you get the jist:
Public Sub CommandButton4_Click()
Dim Name As String
Name = CommandButton4.Caption
Call Sort1
End Sub`
And the other one (Also tried this as function for the sake of trial and error)
Public Sub Sort1(Name As String)
Label11.Caption = Name
Sheets(Name).Select
End Sub
What you're referring to is passing an argument to another subroutine or function. Let's say you want to use a function a lot of times to get the first letter of a string. A sample of this is:
Function LeftOne(StrSample As String) As String
LeftOne = Left(StrSample, 1)
End Function
The above function can be used inside another function or subroutine provided you meet its requirement: StrSample. By declaring StrSample As String in the arguments field of the function, you are basically requiring that any calls to this should require a string to be passed to it. Anything else would throw an error.
The full line LeftOne(StrSample As String) As String can be read as: "I am function LeftOne. Pass me a string and I'll return to you a string after doing something with it." Note that the name StrSample is an arbitrary name.
Anyway, calling the above is as simple as:
Sub MsgInABox()
Dim StrToFeed As String
StrToFeed = "BK201"
MsgBox LeftOne(StrToFeed) 'Returns B.
End Sub
In your example, if you want to pass Name to Sort1, your attempt is absolutely correct.
Let us know if this helps.
You hat to give your sort1 procedure the parameter name.
call sort1(name)
or
call sort1(CommandButton4.Caption)

Resources