VBA global variables, multiple workbooks - excel

I have a VB application, that uses some global variables to store data that is required by multiple forms and modules, this works fine. However if a user opens up another workbook, running the same VBA application, then they end up accessing (and changing) the same public variables.
How can I have workbook level global variables, or if this is not possible, what is the best way to store data that is accessible by all forms and modules, but only inside the specific workbook?
This is a VBA addin, and the Global variables are declared in a standard module.

I was able to replicate the problem with a simple xla called George:
Public harry As Variant
Public Sub setHarry(x)
harry = x
End Sub
Public Function getHarry()
getHarry = harry
End Function
I installed the xla. Then I created Alice.xls with a text box that called setHarry when it changed and a cell with =getHarry() in it. I kept it really simple:
Private Sub TextBox1_Change()
Run "george.xla!setHarry", TextBox1
End Sub
I then made a copy of Alice.xls as Bob.xls and ran them both. As expected, if either workbook changes Harry, both workbooks see the result.
I say expected because xla's are like dll's in that there's only one copy in memory that everybody shares; that apparently includes global variables, which makes sense. I just wanted to test the theory.
To my mind, the best way to fix this is to use a class module instead of an ordinary module. This way you can give each workbook its own instance of the class and its variables in the Workbook_Open event. If the variables are declared public, you don't need property gets and sets, though you can use them if there's value in doing it.

Public myvar as Variant
Variables declared as Public are scoped to the workbook they're in. If you have another workbook with myvar, changing it's value in one won't change it in the other. Perhaps you have an event that's setting that variable from the activesheet that's firing in both projects and setting the variables to the same thing.

Your global variables are globally scoped to your Addin.
I would store them in hidden names in each workbook, and then reset your global variables each time a workbook is activated (and also re-store the values in the deactivated workbook).

I think you could get the workbook name in runtime.
So, a Collection probably will do the job.
'//-- module --
Public var As Collection
Public Function SetVar(Value)
If var Is Nothing Then
Set var = New Collection
End If
var.Add Value, ThisWorkbook.Name
End Function
Public Function GetVar() As Variant
If var Is Nothing Then
GetVar = Null
Else
GetVar = var.Item(ThisWorkbook.Name)
End If
End Function
Not sure if it'll work for your Addin though.

Related

Display alternate UserForm dependent on variable

I want to display a specific UserForm dependent on the UserName environment variable. I update the userform in various places throughout the code so I thought it would be easiest (and trivial) to create two independent userforms (with wildly different designs) then use logic to "Set" a UserForm object variable at the beginning of the code. I'm clearly misunderstanding something here, cause when it gets to the .Show command, VBA errors:
Dim usrForm As UserForm
If Environ("UserName") = "redacted" Then
Set usrForm = LlamaForm 'for specific user, form styled differently including picture of Llama
Else
Set usrForm = NormalForm 'for EVERYONE ELSE, normal professional looking form
End If
With usrForm 'initialize UserForm and display wait message
.Cancelbutton.Visible = False
.Proceedbutton.Visible = False
.Exitbutton.Visible = False
.labmsg.Caption = Chr(10) & Chr(10) & Chr(10) & "Starting background processes, please wait..."
.Show vbModeless
End With
Am I making this too complicated? I was really hoping to just change the referenced form object at the beginning rather than introducing logic with redundant code each time I need to update the user. Any ideas or improvement would be appreciated. Caveat is, because they are wildly different layout/design, I would really like to keep two separate userforms rather than manipulating a single one (which I know can be done, but that is more work at this point compared to understanding why my method above isn't working.)
You can use a generic object, but you lose the early binding features
Try this code
Option Explicit
Public Sub ShowUserForm()
' You can use a generic object, but you lose the early binding features
Dim myUserForm As Object
If Environ("UserName") = "redacted" Then
Set myUserForm = New LlamaForm
Else
Set myUserForm = New NormalForm
End If
myUserForm.Show
End Sub
Let me know if it works
The problem is that the code that's responsible for showing the form, shouldn't have to care for any controls on either form.
Error 438 is being raised, because the UserForm class does not have CancelButton, ProceedButton, ExitButton, or labmsg members.
The solution is to either lose early binding and late-bind these member calls by making them all go against Object (or Variant, but Object is more appropriate here), ...or to re-assess who's responsible for what.
Looks like you're making some kind of progress indicator; when you start needing swappable object components and maintaining early-binding is a necessity, the correct tool to use for that is polymorphism, i.e. interfaces.
The linked article describes how to make a reusable progress indicator whose worker code is fully decoupled from the indicator form. What you need is something like it, except as mentioned near the end of the article you'll want the ProgressIndicatorForm and the LlamaIndicatorForm to implement some IProgressView interface, and have this IProgressView taken in as a dependency of the ProgressIndicator class, rather than it being hard-wired into the initialization code.
The hard part will be managing to expose the progress indicator events on the interface - that'll require a formal ProgressIndicatorEvents class to forward the Activated and Cancelled events to the ProgressIndicator class, similar to how this class forwards events from an abstract view to another component.
Done right, you end up with the calling code looking like this:
Dim progressForm As IProgressView
If Environ$("username") = "redacted" Then
Set progressForm = New LlamaProgressForm
Else
Set progressForm = New StandardProgressForm
End If
With ProgressIndicator.Create("DoWork", Form:=progressForm)
.Execute
End With
Where DoWork is your "worker code" - can be any Sub procedure that takes a ProgressIndicator parameter.
Obviously this is a lot more work than just coding against Object and I wouldn't blame you for taking the simple route. But the principles at play here are well worth looking into, if learning new programming concepts is more important than just getting it to work.

Distribution issues on Bloomberg-enabled spreadsheet

I've created a spreadsheet which optionally uses Bloomberg data pulled using the API COM 3.5 Type Library. I want to be able to distribute that spreadsheet to non-Bloomberg users, but they can't run it since they don't have the right libraries.
All blpapi-related code besides what's in the Class Module is behind if statements that should not be entered by the non-BB users. In the class module, I lazily define the session and Service so that the blpapi-specific definitions are delayed until the class initializes (see below):
Option Explicit
Private session As Object
Dim refdataservice As Object
Private Sub Class_Initialize()
' First create session.
Set session = New blpapicomLib2.session
session.QueueEvents = True
session.Start
' Then open service.
' A service provides the schemas needed to make requests.
session.OpenService "//blp/refdata"
Set refdataservice = session.GetService("//blp/refdata")
End Sub
In short - the code which appears to be causing the issues never runs. My (very limited) understanding of VBA is mostly functional, so I'm probably missing something obvious. Is this a compilation-related error? Is there a way for me to precompile the VBA so users don't experience this issue? Maybe some type of error handling method so the workbook doesn't hang?
Here was my solution for my own problem:
I had two functions with inputs that used library-specific types. I converted those to generic objects (using late binding on the session and refdataservice was not enough). Then, I unselected the bbcom library, and added a dynamic reference to the dll file in Workbook_Open(). Even if the load fails, the error is able to be caught, whereas before excel would have to be killed. This is still a problem if the user tries to use part of the workbook that uses the BB-related code, but this can be mitigated in a few different ways.

Tempvars resetting to null when multithreading

I have a function that I use to multithread some Access VBA routines. It does this by writing a vb script which saves a copy of the main Access database then opens it in another instance of access. when open it runs a defined routine specified in the vb script. this works well but I have some routines where I need to pass variables to them from the main function. I was intending to use Tempvars to do this.
the main function sets up the tempvar (complete with initial values), but when the new access instance goes to read it the tempvar has been set to null. I have no idea why this is happening. It does not error when it refers to the tempvar, so Im assuming that it can see it. but I cann't see the tempvars objects in the locals window anyway. is there a way to monitor the tempvar objects?
I have used tempvars in the past in a similar capacity successfully. the main difference is the new new instance writes to the tempvar and the main function reads them. the issue that Im having is the main function writing to them and the new instance reads them (which is opposite).
TempVars are properties of the Application object, not the database. A new Access.Application will not have access to the TempVars set in an old one.
In my opinion, the better approach is to use forms, timers and COM to achieve this. That way you can pass variables from one Access.Application object to another (including, but not limited to TempVars)
A quick sample (I'm working on a more full approach to multithreading in Access)
Using a blank database with a single form, MTForm, which has a form module:
In MTForms form module:
Private strTask As String
Private Sub Form_Timer()
Application.Run strTask
Application.Quit
End Sub
Public Property Let Task(Value As String)
strTask = Value
End Property
And in a separate module:
Public Sub RunAsync(strFunction As String)
Dim A As New Access.Application
A.OpenCurrentDatabase Application.CurrentProject.FullName
A.DoCmd.OpenForm "mtForm"
A.Forms("mtForm").Task = strFunction
A.Forms("mtForm").TimerInterval = 1
End Sub
Use: RunAsync "MyFunction" will run function MyFunction asynchronously on a separate Access.Application instance of the same database, that quits once execution is done (note that you can't have an exclusive lock on the database, else this will fail).
This can be easily adapted to add parameters, pass values from one thread to another, etc.

"TypeOf...Is Child" from Parent causes broken Excel file

I've been tracking down this issue for several days, so I thought I'd post it here to help others with the same problem, as well as learn more about the cause. I've simplified the problem code to the two class modules at the end of this post.
Basically, the simplified scenario is this: Two class modules, Parent and Child, where Child implements Parent. Somewhere in Parent is the line TypeOf Me Is Child, where Me could be any Object.
From my understanding, when the TypeOf...Is line is compiled to P-code (Debug > Compile, or calling the method) and saved to the file (.xlsm or .xlsb), it causes the file to not open properly. The code will run fine, but when the file is saved, closed, and reopened, it gives an error upon opening (or opening the VBE) saying either Invalid data format or Error accessing file. Network connection may have been lost, and the Parent module can no longer be opened, nor can any VBA be run (try ?1=1 in the Immediate Window and it gives the same error).
If the type is checked using TypeName() instead of TypeOf...Is, this issue does not appear (which is the solution I've used in my project).
Can anyone shed some more light on what exactly is going wrong here, or at least confirm I'm on the right track in terms of what's causing the problem (the P-code)?
PS Yes, I'm aware the parent having knowledge of the child is poor design, but I was near the end of one-off project which wasn't worth taking the time to redesign.
Useful links:
Explains VBA's code states, and what is saved to the file.
http://orlando.mvps.org/VBADecompilerMore.asp?IdC=OrlMoreWin#WhatItIs
Class Modules:
Parent:
Option Explicit
' Class: Parent
' The problem (so far as I can tell):
' When the compiled version of the method below is saved to the file, the file
' will no longer load properly. Upon saving and reopening the file, I get a
' "Invalid data format" error, and the code for this class module can no longer be
' accessed. Furthermore, no VBA code will run after this happens. Try typing "?1=1"
' into the Immediate Window - you'll get another "Invalid data format" window.
' Alternatively, the error will be "Error accessing file. Network connection may
' have been lost." if the code is changed from using "Me" to "tmp" as noted in the
' comments in DoSomething().
' Steps to replicate:
' 1. Debug > Compile VBAProject.
' 2. Save file.
' 3. Close Excel.
' 4. Reopen file (and may need to open VBE).
Public Sub DoSomething()
' The TypeOf...Is statement seems to be what causes the problem.
' Note that checking "Me" isn't the cause of the problem (merely makes
' for shorter demo code); making a "Dim tmp as Object; set tmp = new Collection"
' and checking "TypeOf tmp Is Child" will cause the same problem.
' Also note, changing this to use TypeName() resolves the issue.
' Another note, moving the TypeOf...Is to a "Private Sub DoSomethingElse()" has
' no effect on the issue. Moving it to a new, unrelated class, however, does
' not cause the issue to occur.
If TypeOf Me Is Child Then
Debug.Print "Parent"
End If
End Sub
Child:
Option Explicit
' Class: Child
Implements Parent
Private Sub Parent_DoSomething()
Debug.Print "Child"
End Sub
IMPLEMENTS statement causes circular dependency
The issue is not the TypeOf statement per se. The issue is that you have set up a circular dependency that VBA cannot resolve. As user2140173 mentioned VBA does not truly implement polymorphism.
The circular reference you have created is that the definition of your interface "Parent" includes (requires existence of) your object "Child" and the definition of your class "Child" implements (requires existence of) "Parent". Therefore VBA cannot properly create the interface at compile time and the interface class becomes corrupted and inaccessible next time you have saved, closed and re-opened the workbook and VB Editor.
The OP could be misinterpreted as implicating the statement TypeOf .. Is as being somehow to blame. However, the TypeOf statement is not special. Any statement at all in the interface class that references a class that itself IMPLEMENTS the interface class will set up the circular dependency problem.
For example:
Person.cs
'Class Person
Option explicit
Public Sub SaySomething()
Dim B as Boy '<--- here we cause the problem!
End sub
Boy.cs
'Class Boy
Option explicit
Implements Person
Private Sub Person_SaySomething()
Debug.Print "Hello"
End sub
So i hope you can see that Boy.cs Implements Person.cs which contains a Boy.cs which Implements a Person.cs which contains a Boy.cs .... VBA goes crazy at this point :)
It's a little unfortunate that the VB Editor doesn't give a more helpful error message than the "Invalid data format" error or the "Error accessing file. Network connection may have been lost." which leaves the User baffled!
The solution is to remove these statement(s) from the source code of the interface class. If this proves difficult to do because you have a lot of business logic actually written in the interface class then a useful approach is to move the business logic out to a separate class. Simply doing this on its own can resolve the compile problem with the Interface and get your code running again.
In my own experience, for this very reason I deliberately try to remove any business logic from the interface class to ensure this kind of error cannot occur, and the interface classes become extremely simple - just a list of method signatures. If there is common business logic that I don't want to have to duplicate in each of the classes that will IMPLEMENT my interface then I create an additional class to hold this common business logic and ensure that the interface requires this class to exist.
For example:
iMusicalInstrument.cs
'iMusicalInstrument interface
Option Explicit
Property Get Common() as csMusicalInstrumentCommon
End Property
csMusicalInstrumentCommon.cs
'MusicalInstrumentCommon class
Option Explicit
' add any methods you want to be available to all implementers of the interface.
Property Get GUID() as string '<-- just an example, could be any method
GUID = 'function to create a GUID
End Property
csTrumpet.cs
' csTrumpet class
Option Explicit
Implements iMusicalInstrument
Private mCommon As csMusicalInstrumentCommon
Private Sub Class_Initialize()
Set mCommon = New csMusicalInstrumentCommon
End Sub
Private Sub Class_Terminate()
Set mCommon = Nothing
End Sub
Private Property Get iMusicalInstrument_Common() As csMusicalInstrumentCommon
Set iMusicalInstrument_Common = mCommon
End Property
Usage
Public Sub Test()
Dim Trumpet As New csTrumpet
Dim iTrumpet As iMusicalInstrument
Set iTrumpet = Trumpet
Debug.Print iTrumpet.Common.GUID
End Sub
:)
VBA does not support class polymorphism.
I think you are misunderstanding the purpose of the keyword Implements.
It's used when you want a class to implement an Interface - not another class ( well, at least not literally because an Interface in VBA is another class module object )
See this answer for better understanding of the Implements keyword in VBA
Also refer to this for info about VBA polymorphism

What are the benefits of using Classes in VBA? [closed]

Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 3 years ago.
Improve this question
I am doing some VBA programming in Excel and have one workbook where all the datasheets are to be copied from into another sheet. The new sheet will have several header rows, and I would like to keep track of where they are situated so I don't have to find words in them constantly.
Is the simplest thing to use classes and keep them running while the Excel workbook is open? Or will this make it heavy and hard to handle, and I should keep working with subroutines? What are the benefits of using classes? It is not like I have several objects, only sheets and validation on columns.
The advantage of using classes instead of just subroutines is that classes create a level of abstraction that allow you to write cleaner code. Admittedly, if you've never used classes before in VBA, there is a learning curve, but I believe it's certainly worth the time to figure it out.
One key indication that you should switch to classes is if you're constantly adding parameters to your functions and subroutines. In this case, it's almost always best to use classes.
I've copied an explanation of classes from one of my previous Stack Overflow answers:
Here's a long example of how using a class might help you. Although this example is lengthy, it will show you how a few principles of object-oriented programming can really help you clean up your code.
In the VBA editor, go to Insert > Class Module. In the Properties window (bottom left of the screen by default), change the name of the module to WorkLogItem. Add the following code to the class:
Option Explicit
Private pTaskID As Long
Private pPersonName As String
Private pHoursWorked As Double
Public Property Get TaskID() As Long
TaskID = pTaskID
End Property
Public Property Let TaskID(lTaskID As Long)
pTaskID = lTaskID
End Property
Public Property Get PersonName() As String
PersonName = pPersonName
End Property
Public Property Let PersonName(lPersonName As String)
pPersonName = lPersonName
End Property
Public Property Get HoursWorked() As Double
HoursWorked = pHoursWorked
End Property
Public Property Let HoursWorked(lHoursWorked As Double)
pHoursWorked = lHoursWorked
End Property
The above code will give us a strongly-typed object that's specific to the data with which we're working. When you use multi-dimension arrays to store your data, your code resembles this: arr(1,1) is the ID, arr(1,2) is the PersonName, and arr(1,3) is the HoursWorked. Using that syntax, it's hard to know what is what. Let's assume you still load your objects into an array, but instead use the WorkLogItem that we created above. This name, you would be able to do arr(1).PersonName to get the person's name. That makes your code much easier to read.
Let's keep moving with this example. Instead of storing the objects in array, we'll try using a collection.
Next, add a new class module and call it ProcessWorkLog. Put the following code in there:
Option Explicit
Private pWorkLogItems As Collection
Public Property Get WorkLogItems() As Collection
Set WorkLogItems = pWorkLogItems
End Property
Public Property Set WorkLogItems(lWorkLogItem As Collection)
Set pWorkLogItems = lWorkLogItem
End Property
Function GetHoursWorked(strPersonName As String) As Double
On Error GoTo Handle_Errors
Dim wli As WorkLogItem
Dim doubleTotal As Double
doubleTotal = 0
For Each wli In WorkLogItems
If strPersonName = wli.PersonName Then
doubleTotal = doubleTotal + wli.HoursWorked
End If
Next wli
Exit_Here:
GetHoursWorked = doubleTotal
Exit Function
Handle_Errors:
'You will probably want to catch the error that will '
'occur if WorkLogItems has not been set '
Resume Exit_Here
End Function
The above class is going to be used to "do something" with a colleciton of WorkLogItem. Initially, we just set it up to count the total number of hours worked. Let's test the code we wrote. Create a new Module (not a class module this time; just a "regular" module). Paste the following code in the module:
Option Explicit
Function PopulateArray() As Collection
Dim clnWlis As Collection
Dim wli As WorkLogItem
'Put some data in the collection'
Set clnWlis = New Collection
Set wli = New WorkLogItem
wli.TaskID = 1
wli.PersonName = "Fred"
wli.HoursWorked = 4.5
clnWlis.Add wli
Set wli = New WorkLogItem
wli.TaskID = 2
wli.PersonName = "Sally"
wli.HoursWorked = 3
clnWlis.Add wli
Set wli = New WorkLogItem
wli.TaskID = 3
wli.PersonName = "Fred"
wli.HoursWorked = 2.5
clnWlis.Add wli
Set PopulateArray = clnWlis
End Function
Sub TestGetHoursWorked()
Dim pwl As ProcessWorkLog
Dim arrWli() As WorkLogItem
Set pwl = New ProcessWorkLog
Set pwl.WorkLogItems = PopulateArray()
Debug.Print pwl.GetHoursWorked("Fred")
End Sub
In the above code, PopulateArray() simply creates a collection of WorkLogItem. In your real code, you might create class to parse your Excel sheets or your data objects to fill a collection or an array.
The TestGetHoursWorked() code simply demonstrates how the classes were used. You notice that ProcessWorkLog is instantiated as an object. After it is instantiated, a collection of WorkLogItem becomes part of the pwl object. You notice this in the line Set pwl.WorkLogItems = PopulateArray(). Next, we simply call the function we wrote which acts upon the collection WorkLogItems.
Why is this helpful?
Let's suppose your data changes and you want to add a new method. Suppose your WorkLogItem now includes a field for HoursOnBreak and you want to add a new method to calculate that.
All you need to do is add a property to WorkLogItem like so:
Private pHoursOnBreak As Double
Public Property Get HoursOnBreak() As Double
HoursOnBreak = pHoursOnBreak
End Property
Public Property Let HoursOnBreak(lHoursOnBreak As Double)
pHoursOnBreak = lHoursOnBreak
End Property
Of course, you'll need to change your method for populating your collection (the sample method I used was PopulateArray(), but you probably should have a separate class just for this). Then you just add your new method to your ProcessWorkLog class:
Function GetHoursOnBreak(strPersonName As String) As Double
'Code to get hours on break
End Function
Now, if we wanted to update our TestGetHoursWorked() method to return result of GetHoursOnBreak, all we would have to do as add the following line:
Debug.Print pwl.GetHoursOnBreak("Fred")
If you passed in an array of values that represented your data, you would have to find every place in your code where you used the arrays and then update it accordingly. If you use classes (and their instantiated objects) instead, you can much more easily update your code to work with changes. Also, when you allow the class to be consumed in multiple ways (perhaps one function needs only 4 of the objects properties while another function will need 6), they can still reference the same object. This keeps you from having multiple arrays for different types of functions.
For further reading, I would highly recommend getting a copy of VBA Developer's Handbook, 2nd edition. The book is full of great examples and best practices and tons of sample code. If you're investing a lot of time into VBA for a serious project, it's well worth your time to look into this book.
If there are lots of subroutines or subroutines are very long then structuring the code in to classes may help. If there are only a couple of subroutines, say, each being only 10 lines of code each then this is over kill. The benefit of structuring the code in to classes is that it is easier to read and change when you come back to it down the line. So another reason to structuring the code into classes is if the code is likely to need changing down the line
There is one other thing you could add to the advantages other contributors have stated (sorry if it's somewhere in Ben McCormack's excellent answer and I missed it). Classes can have their uses if your VBA script is likely to be re-programmed at some point.
For instance, I am designing a sort of order management system. It is to be used by several colleagues for quite a while, but it may need re-progamming if ordering rules change. I have therefore designed a basic stock item class, which gathers all the information about a stock item. The rules about how this data is analyzed for any order are, however, written in easily accessible and well commented sub routines. By doing this, I hope that future VBA programmers can easily change the mathematical rules by which orders are generated, without having to deal with how all the data is gathered about a particular stock item (this is all done by subroutines and functions within the class, which are activated when the class is handed a stock number). A Class' public properties are also picked up by intellisense, allowing the next programmer, as well as yourself, to have an easier time of it.
I guess the point is that classes can make life easier for later users in this way if they encode some basic set of information, or some conceptual object, that is always likely to be relevant to the context of the program's use.

Resources