Public vs Private/Dim in Excel VBA - vba

I could use some help in understanding using Public vs Dim in a module in Excel 2013 VBA.
First I want to say I did find this great post with excellent definitions (see link below), but no examples and I could use some examples of how I could apply the Public Variables to my project. Also I am a little confused on when I would need to use the Option Private Module; would I need to use that on each module I have or just the module that holds the below code?
stackoverflow descriptions difference between Public/Private
What I would like to do is set this up in a Standard Mod so I dont have to continue setting variables for worksheets through all of my UserForms that use the same naming convention for Worksheets they reference.
Sub PubVar()
Public wb As Workbook
Public wsSI As Worksheet
Public wsRR As Worksheet
Public wsCalcs As Worksheet
Public wsNarr As Worksheet
Public wsEval As Worksheet
Public wsUW As Worksheet
Public wsLVBA As Worksheet
Set wb = Application.ThisWorkbook
Set wsSI = wb.Sheets("SavedInfo")
Set wsCalcs = wb.Sheets("Calcs")
Set wsNarr = wb.Sheets("Narrative")
Set wsEval = wb.Sheets("EvalCL")
Set wsUW = wb.Sheets("UWCL")
Set wsLVBA = wb.Sheets("ListsForVBA")
End Sub
Thank you for your assistance.

Option Private Module
Option Private Module should be used in any standard module that doesn't mean to expose its public members to Excel as macros (i.e. Public Sub procedures) or User-Defined-Functions (i.e. Public Function procedures).
Without this option, a standard module's public parameterless Sub procedures appear in Excel's list of available macros, and public Function procedures appear in Excel's cell "intellisense" as available worksheet functions.
Note that this merely hides a module's members from the macros list: if you type the exact name of a "hidden" procedure, Excel will still run it.
Dim vs Private vs Public
Dim is a keyword you use for declaring local variables, inside a procedure scope. The keyword is also legal for declaring private, module-level variables, but then you might as well use Private.
When used for declaring module-level variables, Private makes that variable only accessible from within the module it's declared in.
When used for declaring module-level variables, Public makes that variable accessible from anything that has access to the module it's declared in - in a standard module, that means the variable is effectively Global. In a class (/document/userform/anything else) module, it means the variable holds instance state and is accessible from anything that has access to an instance of that class. Classes that have a predeclaredId, such as UserForm classes, all have an instance that's globally accessible: avoid storing instance state in this default instance.
Use Worksheet.CodeName
Set wb = Application.ThisWorkbook
Set wsSI = wb.Sheets("SavedInfo")
Set wsCalcs = wb.Sheets("Calcs")
Set wsNarr = wb.Sheets("Narrative")
Set wsEval = wb.Sheets("EvalCL")
Set wsUW = wb.Sheets("UWCL")
Set wsLVBA = wb.Sheets("ListsForVBA")
ThisWorkbook is the workbook you're looking at - the one that contains your VBA code. The ThisWorkbook identifier is globally accessible, and Application.ThisWorkbook is merely a pointer to that object.
Use ThisWorkbook over Application.ThisWorkbook, unless you've declared a local variable and named it ThisWorkbook - then that local variable would be shadowing the global identifier; don't do that. There shouldn't be any reason to need to qualify ThisWorkbook with Application.
Now, if any of these worksheets exist at compile-time in ThisWorkbook, then you don't need any of these variables. Find each sheet in the Project Explorer (Ctrl+R), then hit F4 and give its (Name) property a meaningful identifier name.
So if you rename Sheet1 to SavedInfoSheet, then you can access SavedInfoSheet from anywhere in the code, and you don't ever need to dereference it from the Workbook.Sheets (or better, Workbook.Worksheets) collection. The reason for this is that VBA automatically creates a global-scope identifier by the name of whatever identifier you put as the (Name) property of a Worksheet module.
If the sheets don't exist at compile-time (i.e. they're created at run-time), then you don't need these variables either, because the code that created them should already have that reference:
Set theNewSheet = theBook.Worksheets.Add
Then you can (and should) pass these worksheet object references around, as parameters, as needed.
There is no worksheet.
What I would like to do is set this up in a Standard Mod so I dont have to continue setting variables for worksheets through all of my UserForms that use the same naming convention for Worksheets they reference
Your forms are running the show. The code that fires them looks like this:
UserForm1.Show
Like any UI, forms are responsible for collecting user input, and showing data to the user. If you find yourself writing userform code-behind that accesses a dozen worksheets (and/or worse, makes them public fields), you're making your form much, much more complicated than it needs to be, and you're treating a full-fledged object as a mere container for procedures, by making its default instance stateful.
This article goes in details about how to fix that. This article pushes the concept further and allows back-and-forth communication between the view and the presenter, and has a download link with a simple example to study (disclaimer: I wrote these articles, and the accompanying example code).
UserForm code done right, looks extremely simple, and is responsible for so little logic, it's boring. In fact, it's not responsible for any logic beyond presentation - all a UserForm should do, is respond to control events, relay control state to some model, and if application logic needs to be executed before the form is closed (e.g. if a command button is clicked but the form should remain open), then it fires an event, and the calling code ("presenter") handles it by triggering the logic that needs to run.
When the dialog is okayed, or when it relays an event to the presenter, code outside the form's code-behind is executed to to the work: the form never needs to know anything about any worksheet.

You really should only need Option Private Module if you are making your own Excel Add-In (*.XLAM file). It allows a module's Public variables to be visible to other modules within the Add-In's own project but keeps them from being visible/callable by other worksheets that use your AddIn.
As for the rest, it's not clear what you are asking. The question you linked has a good explanation of Public vs Dim/Private. Private/Dim variables cannot be seen by the VBA code in different Modules, Forms or Classes in the same VBA project. You use public if you want all of your VBA code to be able to see/call it. You use private/dim if you want it to only be visible/callable from within it's own module.
But generally you want to be judicious with what you make public, both because it can give you too many global names (confusing), can cause problems with duplicate names (problematic, can be addressed with naming standard) and most of all because it can lead to confusing/obscure bugs and horrendous debugging problems (because any public variable change could cause any other code that can see it to behave differently). And some things are worse than others:
Public Subs OK in a module, but better in a Class
Public Function Usually fine, especially if its a true function
Public Const No problem, belongs in modules
Public Variables Very Bad. *UNLESS ...*
Public variables are the ones that good coders worry about. They are to be avoided, UNLESS they are what we call "Read-Only Variables". These are variables that you set only once, and never change again. As long as you follow that rule, they are effectively constants and are OK (though read-only static properties would be better, but VBA has too many limitations in this regard).
Finally If you want to use named variables for all of your worksheets that are visible/useable from all code, but you only have to setup once, that is what Public is for, but the declaration ("Public ") needs to be at "the Module Level" which means, in a module, but outside of any subroutine or function. Like this:
Public wb As Workbook
Public wsSI As Worksheet
Public wsRR As Worksheet
Public wsCalcs As Worksheet
Public wsNarr As Worksheet
Public wsEval As Worksheet
Public wsUW As Worksheet
Public wsLVBA As Worksheet
' sets all of the Worksheet variables
Sub PubVar()
Set wb = Application.ThisWorkbook
Set wsSI = wb.Sheets("SavedInfo")
Set wsCalcs = wb.Sheets("Calcs")
Set wsNarr = wb.Sheets("Narrative")
Set wsEval = wb.Sheets("EvalCL")
Set wsUW = wb.Sheets("UWCL")
Set wsLVBA = wb.Sheets("ListsForVBA")
End Sub

Related

In VBA should code that modifies document be avoided in a class module

I am starting to use Classes in VBA and appreciate some of the fantastic information that is already available on SO.
As far as I can tell, what seems to be lacking is an explanation of what the code in a class should or, as I suspect, should NOT do. For example:
Lets say I have a document and wish to insert / modify a table. In this example I'd like to:
check if the table exists
if table does not exist:
add the table at a specific location
add information to the table (i.e. add rows)
if table does exist
add / delete information to /from the table
sort the table according to some criteria
With respect to 'sorting' I imagine that a class module would be well suited to determining the order that information should be put into a table based on some criteria.
But ideally:
Should the class module (or a 2nd class module) be used to check and edit the document?
OR
Would checking and/or editing be best done using a regular module?
Or doesn't it matter? If there is a preferred way then what are the advantages / disadvantages of each approach?
First off, kudos for entering the wonderful rabbit hole of OOP!
Short answer: It depends.
(very) long answer:
You'll want to avoid pulling a worksheet [that exists at compile-time] from the Application.Worksheets (or Application.Sheets) collection, and use that sheet's CodeName instead. VBA creates a global-scope object reference for you to use, named after every worksheet's CodeName.
That's how this code gets to compile, with Sheet1 not being declared anywhere:
Option Explicit
Sub Test()
Debug.Print Sheet1.CodeName
End Sub
The problem with implementing worksheet-specific functionality anywhere other than in that worksheet's code-behind, using that global-scope "free" object variable, is that the separate module is now coupled with that Sheet1 object.
Class module depending on a worksheet. Any worksheet.
You want focused, cohesive modules - high cohesion. And low coupling.
By writing worksheet-specific code in another module (be it a standard or a class module), you're creating a dependency and increasing coupling, which reduces testability - consider this code in Class1:
Public Sub DoSomething()
With Sheet1
' do stuff
End With
End Sub
Now Class1 can only ever work with Sheet1. This would be better:
Public Sub DoSomething(ByVal sheet As Worksheet)
With sheet
' do stuff
End With
End Sub
What happened here? Dependency Injection. We have a dependency on a specific sheet, but instead of coding against that specific object, we tell the world "give me any worksheet and I'll do my thing with it". That's at method level.
If a class means to work with a single specific worksheet, and exposes multiple methods that do various things with that worksheet, having a ByVal sheet As Worksheet parameter on every single method doesn't make much sense.
Instead you'll inject it as a property:
Private mSheet As Worksheet
Public Property Get Sheet() As Worksheet
Set Sheet = mSheet
End Property
Public Property Set Sheet(ByVal value As Worksheet)
Set mSheet = value
End Property
And now all methods of that class can work with Sheet... the only problem is that the client code consuming that class now needs to remember to Set that Sheet property, otherwise errors can be expected. That's bad design IMO.
One solution could be to push the Dependency Injection Principle a notch further, and actually depend on abstractions; we formalize the interface we want to expose for that class, using another class module that will act as the interface - that IClass1 class doesn't implement anything, it just defines stubs for what's exposed:
'#Interface
Option Explicit
Public Property Get Sheet() As Worksheet
End Property
Public Sub DoSomething()
End Sub
Our Class1 class module can now implement that interface, and if you've been following this far, hopefully I don't lose you here:
NOTE: Module and member attributes are not visible in the VBE. They're represented here with their corresponding Rubberduck annotations.
'#PredeclaredId
'#Exposed
Option Explicit
Implements IClass1
Private mSheet As Worksheet
Public Function Create(ByVal pSheet As Worksheet) As IClass1
With New Class1
Set .Sheet = pSheet
Set Create = .Self
End With
End Function
Friend Property Get Self() As IClass1
Set Self = Me
End Property
Private Property Get IClass1_Sheet() As Worksheet
Set IClass1_Sheet = mSheet
End Property
Private Sub IClass1_DoSomething()
'implementation goes here
End Sub
This Class1 class module presents two interfaces:
Class1 members, accessible from the PredeclaredId instance:
Create(ByVal pSheet As Worksheet) As IClass1
Self() As IClass1
IClass1 members, accessible from the IClass1 interface:
Sheet() As Worksheet
DoSomething()
Now the calling code can look like this:
Dim foo As IClass1
Set foo = Class1.Create(Sheet1)
Debug.Assert foo.Sheet Is Sheet1
foo.DoSomething
Because it's written against the IClass1 interface, the calling code only "sees" the Sheet and DoSomething members. Because of the VB_PredeclaredId attribute of Class1, the Create function can be accessed via the Class1 default instance, pretty much exactly like Sheet1 gets accessed without creating an instance (UserForm classes have that default instance, too).
This is the factory design pattern: we're using the default instance as a factory whose role is to create and initialize an implementation of the IClass1 interface, which Class1 just so happens to be implementing.
With Class1 completely decoupled from Sheet1, there's absolutely nothing wrong with having Class1 responsible for everything that needs to happen on whatever worksheet it's initialized with.
Coupling is taken care of. Cohesion is another problem entirely: if you find Class1 is growing hair and tentacles and becomes responsible for so many things you don't even know what it was written for anymore, chances are that the Single Responsibility Principle is taking a beating, and that the IClass1 interface has so many unrelated members that the Interface Segregation Principle is also taking a beating, and the reason for that is probably because the interface wasn't designed with the Open/Closed Principle in mind.
The above couldn't be implemented with standard modules. Standard modules don't play quite well with OOP, which means tighter coupling and thus lower testability.
TL;DR:
There isn't one single "right" way to design anything.
If your code can deal with being tightly coupled with a specific worksheet, prefer implementing the functionality for that worksheet in that worksheet's code-behind, for better cohesion. Still use specialized objects (classes) for specialized tasks: if your worksheet code-behind is responsible for setting up a database connection, sending a parameterized query over the network, retrieving the results and dumping them into the worksheet, then you're Doing It Wrong™ and now testing that code in isolation, without hitting the database, is impossible.
If your code is more complex and can't afford tight coupling with a specific worksheet, or if the worksheet doesn't exist at compile-time, implement the functionality in a class that can work with any worksheet, and have a class that's responsible for the model of that runtime-created sheet.
IMO a standard module should only be used to expose entry points (macros, UDFs, Rubberduck test methods, and with Option Private Module, some common utility functions), and contain fairly little code that merely initializes objects and their dependencies, and then it's classes all the way down.

Storing range reference in a global variable

I'm using several named ranges located in different worksheets. I need to read from and write to those ranges in many situations throughout my VBA code.
So my question is: what is the proper way to store those range references in global variables for quick access? Or is there a better way?
I tried declaring global variables:
Public WS_BOARD As Worksheet
Public RNG_BOARD As Range
and initializing them on Workbook_Open:
Private Sub Workbook_Open()
Set WS_BOARD = Worksheets("BOARD")
Set RNG_BOARD = WS_BOARD.Range("NR_BOARD")
End Sub
This works okay except that if my code crashes those global variables are reset to Nothing and cannot be used any further (i.e. in worksheet event handlers).
Of course, I can always use something like
Worksheets("BOARD").Range("NR_BOARD")
everywhere in my code but I think it will affect performance because it obviously needs to lookup objects using string names, let alone it being not DRY.
One way is to "load" them once into memory and then pass them as arguments into other subs/functions.
Passing Variables By Reference And By Value
Second, you can declare them as module-scope variables (something like public, but only in the module you declare them).
Scope of variables in Visual Basic for Applications
Third, your way.
Scope of variables in Visual Basic for Applications
Every worksheet has a Name property (the tab caption) and a CodeName property. If you use the CodeName in your code, you don't need a variable. Like a userform, a sheet's code name is auto instantiated when you use it. Select the worksheet under your project in the Project Explorer (Ctrl+R), go to the Propeties (F4) and change the (Name) to a meaninful CodeName. The Project Explorer will then show the sheet as
wshDataEntry (Data Entry)
if you made the CodeName wshDataEntry and the tab said Data Entry.
For Ranges, I used defined names in the spreadsheet. It's like a global variable that never goes out of scope. If you have, say, and interest rate cell that you need to read in various procedure, name it Interest_Rate. Then at the top of every procedure where you need it
Set rInterest = wshDataEntry.Range(gsNMINTEREST)
where gsNMINTEREST is a global string variable holding "Interest_Rate". If the cell ever moves, you only need to move your named range for the code to work. If you change the name to "LiborPlusFour" you only need to update your global variable. Pretty DRY.
Yes there is a slight performance hit making VBA look up the named range every time. I wouldn't worry about that unless you've calculated a performance problem and identified it as the drag.
One way that I keep global variables (of course I'm very judicious using them) in scope is an Initialize procedure.
Public Sub Initialize()
If gclsApp Is Nothing Then
Set gclsApp = New CApp
'do some other setup
End If
End Sub
Then I call the Initialize procedure at the top of any module I need it. If it's Not Nothing, then it doesn't reset the variable. I'm not sure the performance implications of checking for Nothing and just looking up a named range, though.
Global variables are generally not a good idea. In some cases they're necessary, but this isn't one of those cases.
I don't think you should worry about the performance impact of looking up a few ranges by their string name. I have sheets with hundreds of named ranges, and I have never, ever observed this having a negative impact on performance.
Do you have specific evidence indicating that this is slowing down your code? If not, then don't waste time worrying about it.
I often do this to define ranges:
Type RangesType
board As Range
plank As Range
wood As Range
End Type
Function GetRanges() As RangesType
With GetRanges
Set .board = Range("board")
Set .plank = Range("plank")
Set .wood = Range("wood")
End With
End Function
Then I use them like this:
Sub something()
Dim rngs As RangesType: rngs = GetRanges
rngs.board = "My favourite board"
rngs.wood = "Chestnut"
'etc.
End Sub
Yes, ranges will get fetched by their name repeatedly if you have many subs, and no, this isn't likely to have any noticeable effect on performance.

Missing VBA compiler message for wrong method name

Consider the following code:
Public Sub VBACompilerIsMad()
Dim Ap As Application
Dim Wb As Workbook
Dim Ws As Worksheet
Debug.Print Ap.XXX ' No compile error
Debug.Print Wb.XXX ' No compile error
Debug.Print Ws.XXX ' Compile error
End Sub
When I compile this, I get a compiler error for referring to an inexisting member of Worksheet. However, if I comment out the last line, there is no compiler error, even though neither Application nor Workbook have a method or property XXX. It is as if I declared Ap and Wb as Object variables.
Why does the compiler treat Application / Workbook differently from Worksheet?
Are there any other classes like this, that the compiler seems to treat as if they were Object?
As I have been explained (kudos go respectively), this is a COM feature.
By default COM assumes an interface is extensible, that is, it allows adding members at run time. If that is not the desired behaviour, one can apply the [nonextensible] attribute to the interface definition, which declares the interface only accepts methods explicitly defined in the type library.
dispinterface _Application and dispinterface _Workbook do not have this flag set in the Excel type library, dispinterface _Worksheet does.
Similarly, ADO's dispinterface _Connection does not have [nonextensible], dispinterface _Command does.
To learn which are extensible, add a reference to TypeLib Info in the project's References and run:
Dim t As tli.TLIApplication
Set t = New tli.TLIApplication
Dim ti As tli.TypeLibInfo
Set ti = t.TypeLibInfoFromFile("excel.exe")
Dim i As tli.InterfaceInfo
For Each i In ti.Interfaces
If (i.AttributeMask And tli.TYPEFLAG_FNONEXTENSIBLE) <> tli.TYPEFLAG_FNONEXTENSIBLE Then
Debug.Print i.Name
End If
Next
You will see that almost all interfaces are extensible here, so most of them get pushed out of the debug window and you will only see the last ones. Change the <> to = to print those that are not extensible, there are much less of them.
A bit of a hypothesis:
You can call a stored procedure on an ADODB.Connection object like a native method (at the bottom).
(The examples for this on several msdn sites look oddly messed up).
So there is some mechanism like 'anonymous/dynamic methods' in VBS/VBA.
It may be a similar mechanism activated here for Application and Workbook classes - although I don't see where and how exactly.
A test supports the basic idea:
I have tested this with a reference to Microsoft ActiveX Data Objects 2.8 Library:
Public Sub testCompiler()
Dim cn As ADODB.Connection
Dim cmd As ADODB.Command
Debug.Print cn.XXX
Debug.Print cmd.XXX
End Sub
cn.XXX does not throw a compile error, cmd.XXX does.
GSerg's answer is indeed outstanding, I love the whole COM type library IDL and how some attributes there can govern the behaviour in the Excel VBA IDE. Long may this arcane knowledge of COM be handed down! And, I realise this question has been bountied to give that answer more rep but when a bounty is set it appears on my radar and I have a view on this matter.
So although GSerg's answer gives the mechanism it does not give the rationale, i.e. it gives the how but not the why. I'll attempt to answer the why.
Some of the answer why is already given by Martin Roller (OP) in his comments about Application and WorksheetFunction. This, to me, is a convincing reason to keep Application extensible and I'll not consider Application further.
Let us turn to Workbook and Worksheet and we best start with some code to demonstrate, so you will need to begin with two fresh workbooks, call them MyWorkbook.xlsm and OtherWorkbook.xlsm. So some instructions:
In OtherWorkbook.xlsm go the code module ThisWorkbook and paste the code
Option Explicit
Public Function SomeFunctionExportedOffOtherWorkbook() As String
SomeFunctionExportedOffOtherWorkbook = "Hello Matt's Mug!"
End Function
In MyWorkbook.xlsm go the Sheet1 code module and paste the code
Option Explicit
Public Function SomeFunctionExportedOffCodeBehindSheet1() As String
SomeFunctionExportedOffCodeBehindSheet1 = "Hello Martin Roller!"
End Function
Now, in the VBA IDE change the codename of Sheet1 to codebehindSheet1
Now, in a new standard module in MyWorkbook.xlsm add the following code
Sub TestingObjectLikeInterfacesOfWorkbookAndCodeBehindWorksheet_RunMany()
'* For this example please rename the 'CodeName' for Sheet1 to be "codebehindSheet1" using the IDE
Debug.Assert ThisWorkbook.Worksheets.Item("Sheet1").CodeName = "codebehindSheet1"
Dim wb As Workbook
Set wb = Application.Workbooks.Item("OtherWorkbook")
'* Workbook dispinterface needs to not marked with nonextensible attribute
'* so that it doesn't trip up over exported function in another workbook
'* below SomeFunctionExportedOffOtherWorkbook is defined in the ThisWorkbook module of the workbook "OtherWorkbook.xlsm"
Debug.Print wb.SomeFunctionExportedOffOtherWorkbook
'*Not allowed --> Dim foo As Sheet1
'*have to call by the 'code behind' name which is usually Sheet1 but which we changed to illustrate the point
Debug.Print codebehindSheet1.SomeFunctionExportedOffCodeBehindSheet1
End Sub
Now run this code above.
You've probably read the code and hopefully understood the point I'm making but let me spell it out. We need Workbook to remain extensible because it may contain a reference to another workbook which may be exporting a method or function and we'd like no compile errors.
However, for the Worksheet, to do a similar export we again add code to the code behind module but there is a difference in referencing the module: one grabs a reference to that code behind module by using its VBA code name, most people do not change this from Sheet1 (that is why you were invited to change it above).
So the interface obtained by the code behind module name needs to extensible and not the Excel.Worksheet interface.
P.S. Anyone got a copy of TLI.dll?
As a workaround it could still be possible to create your own interface and implement this interface. Then declare a variable as INewInterface and all the compiler messages will be there :). Here simple example with custom interface for a UserForm. HTH
Interface
Public CancelButton As MSForms.CommandButton
Public DataList As MSForms.ListBox
Public CommandBox As MSForms.TextBox
Implementation
Implements IMyForm
Private Property Set IMyForm_CancelButton(ByVal RHS As MSForms.ICommandButton)
End Property
Private Property Get IMyForm_CancelButton() As MSForms.ICommandButton
End Property
Private Property Set IMyForm_CommandBox(ByVal RHS As MSForms.IMdcText)
End Property
Private Property Get IMyForm_CommandBox() As MSForms.IMdcText
End Property
Private Property Set IMyForm_DataList(ByVal RHS As MSForms.IMdcList)
End Property
Private Property Get IMyForm_DataList() As MSForms.IMdcList
End Property
Usage
Note: MyForm is existing VBA Form which has been added to the project.

Public variables are not REALLY public in VBA in Forms

Below is a question that I will answer myself, however it caused a GREAT deal of frustration for me and I had a lot of trouble searching for it on the web, so I am posting here in hopes of saving some time & effort for others, and maybe for myself if I forget this in the future:
For VBA (in my case, MS Excel), the Public declaration is supposed to make the variable (or function) globally accessible by other functions or subroutines in that module, as well as in any other module.
Turns out this is not true, in the case of Forms, and I suspect also in Sheets, but I haven't verified the latter.
In short, the following will NOT create a public, accessible variable when created in a Form, and will therefore crash, saying that the bYesNo and dRate variables are undefined in mModule1:
(inside fMyForm)
Public bYesNo As Boolean`
Public dRate As Double
Private Sub SetVals()
bYesNo = Me.cbShouldIHaveADrink.value
dRate = CDec(Me.tbHowManyPerHour.value)
End Sub
(Presume the textbox & checkbox are defined in the form)
(inside mModule1)
Private Sub PrintVals()
Debug.Print CStr(bYesNo)
Debug.Print CStr(dRate)
End Sub
However, if you make the slight alteration below, it all will work fine:
(inside fMyForm)
Private Sub SetVals()
bYesNo = Me.cbShouldIHaveADrink.value
dRate = CDec(Me.tbHowManyPerHour.value)
End Sub
(Presume the textbox & checkbox are defined in the form)
(inside mModule1)
Public bYesNo As Boolean`
Public dRate As Double
Private Sub PrintVals()
Debug.Print CStr(bYesNo)
Debug.Print CStr(dRate)
End Sub
mModule1 will work perfectly fine and, assuming that the fMyForm is always called first, then by the time the PrintVals routine is run, the values from the textbox and checkbox in the form will properly be captured.
I honestly cannot possibly fathom what MS was thinking with this change, but the lack of consistency is a huge suck on efficiency, learning idiosyncracies like these, which are so poorly documented that a Google search in 2013 for something that has likely been around for a decade or more is so challenging to search.
First comment:
Userform and Sheet modules are Object modules: they don't behave the same way as a regular module. You can however refer to a variable in a userform in a similar way to how you'd refer to a class property. In your example referring to fMyForm.bYesNo would work fine. If you'd not declared bYesNo as Public it wouldn't be visible to code outside of the form, so when you make it Public it really is different from non-Public. – Tim Williams Apr 11 '13 at 21:39
is actually a correct answer...
As a quick add-on answer to the community answer, just for a heads-up:
When you instantiate your forms, you can use the form object itself, or you can create a new instance of the form object by using New and putting it in a variable. The latter method is cleaner IMO, since this makes the usage less singleton-ish.
However, when in your userform you Call Unload(Me), all public members will be wiped clean. So, if your code goes like this:
Dim oForm as frmWhatever
Set oForm = New frmWhatever
Call oForm.Show(vbModal)
If Not oForm.bCancelled Then ' <- poof - bCancelled is wiped clean at this point
The solution I use to prevent this, and it is a nice alternative solution for the OP as well, is to capture all IO with the form (i.e. all public members) into a separate class, and use an instance of that class to communicate with the form. So, e.g.
Dim oFormResult As CWhateverResult
Set oFormResult = New CWhateverResult
Dim oForm as frmWhatever
Set oForm = New frmWhatever
Call oForm.Initialize(oFormResult)
Call oForm.Show(vbModal)
If Not oFormResult.bCancelled Then ' <- safe
There are other limitations to Public within Excel VBA.
MSoft documentation in learn.microsoft.com states that public variables are global to the VBA project - it's not true.
Public variables are only global to the workbook within which they are declared, and then only across standard modules. Public variables declared within workbook code are not visible in standard modules, even though standard module sub's are - which are defined to be public.
Public variables declared in one workbook's standard modules are certainly not accessible from other workbooks in the same VBA project, contrary to the MSoft documentation.

How to prevent VBA variables from being shared across Word documents?

I have a VBA template project that runs automatically when a Word document is opened. However, if I open multiple documents, they all share the variables values. How can declare these variables to be only associated with the active window or active document?
I tried declaring them in a Class Module, but that did not help. Switching between opened document I can see that these variables are shared.
Any input is appreciated...
This what I have in my Module:
Option Private Module
Dim CurrentCommand As String
Public Function SetCurrentCommand(command)
CurrentCommand = command
End Function
Public Function GetCurrentCommand()
GetCurrentCommand = CurrentCommand
End Function
More Info: The code/Macro start at AutoExec like this:
Public Sub Main()
Set oAppClass.oApp = Word.Application
If PollingRate <> "" Then Application.OnTime Now + TimeValue(PollingRate), "CaptureUserViewState"
End Sub
And the CaptureUserViewState is a Sub that resides in a different Module and does all teh checks (comparing new values to last recorded ones) and here how this Sub does the check:
If WL_GetterAndSetter.GetLastPageVerticalPercentage <> pageVerticalPercentScrolled Then
'Update the last value variable
WL_GetterAndSetter.SetLastPageVerticalPercentage (pageVerticalPercentScrolled)
'log change
End If
You don't give us much information, but I assume you declared public variables at module level like this:
Public myString As String
Public myDouble As Double
From VBA documentation:
Variables declared using the Public statement are available to all procedures in all modules in all applications unless Option Private Module is in effect; in which case, the variables are public only within the project in which they reside.
The answer is to use Option Private Module.
When used in host applications that allow references across multiple projects, Option Private Module prevents a module’s contents from being referenced outside its project.
[...] If used, the Option Private statement must appear at module level, before any procedures.
EDIT You have now clarified that you declare your variables using Dim at module level. In this case, Option Private Module is irrelevant.
Variables declared with Dim at the module level are available to all procedures within the module.
i.e. regardless of whether you're using Option Private Module or not.
If you're finding that the values are retained between runs, then that must be because you are running a procedure from the same module from the same workbook. You may think you're doing something else, but in reality this is what you're doing.
EDIT
In your class module, instead of Dim CurrentCommand As String try Private CurrentCommand As String. Without more information it's hard to debug your program. I'm just taking random potshots here.
What you need to do is store multiple versions of the variables, one set per document.
So I would suggest that you create a simple class to hold the different values.
You then store them in a collection mapping the data-set with the document name or similar as the key.
In classmodule (MyData), marked as public:
Public data1 as String
Public data2 as Integer
In module with the event-handlers:
Dim c as new Collection 'module global declaration
Sub AddData()
Dim d as new MyData 'Your data set
d.data1 = "Some value"
d.data2 = 42
c.add Value:=d, Key:=ActiveDocument.name
End Sub
Then when you enter the event-handler you retrieve the data and use the specific set for the currently active document.
Sub EventHandler()
Dim d as MyData
set d = c.item(ActiveDocument.name)
'use data
'd.data1...
End Sub
Please not that this code is just on conceptual level. It is not working, You have to apply it to your problem but it should give you some idea on what you need to do. You will need to add alot of error handling, checking if the item is already in the collection and so on, but I hope you understand the concept to continue trying on your own.
The reason for this is because, as I understand the situation from your question, you only have one version of your script running, but multiple documents. Hence the script have to know about all the different documents.
On the other hand, If each document would have their own code/eventhandlers, hence having multiple versions of the script running, then you don't need the solution provided above. Instead you need to be careful what document instance you reference in your script. By always using "ThisDocument" instead of "ActiveDocument" you could achieve isolation if the code is placed in each open document.
However, as I understood it, you only have one version of the script running, separate from the open documents, hence the first solution applies.
Best of luck!
You might want to store the Document Specific details using
The Document.CustomDocumentProperties Property
http://msdn.microsoft.com/en-us/library/office/aa212718(v=office.11).aspx
This returns a
DocumentProperties Collection
Which you can add new Properties to Using
Document.CustomDocumentProperties.Add(PropertyName, LinkToContent, Value, Type)
And then Read From using
Document.CustomDocumentProperties.Item(PropertyName)
A downside, or bonus, here is that the properties will remain stored in the document unless you delete them.
This may be a good thing or a bad thing