Using predeclared classes in VBA as namespaces - excel

One of my biggest issues with coding in VBA is a total lack of namespaces making it difficult to ensure things like scoping and selecting the right function when every function of the same name is in the global namcespace. I know you can prefix your function calls with the module name it is in, but it seems to me that this is also possible by replacing all your modules by predeclared classes. A namespace would look something like this:
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "MyNamespace"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = False
'#Folder("Project.Namespace")
Option Explicit
Public Function Foo() As String
Foo = "bar"
End Function
' Opionally make sure users do not use this as a class
Private Sub Class_Initialize()
If Not Me Is MyNamespace Then
Err.Raise 512, _
Source:="MyNamespace.Initialize", _
Description:="Cannot create an instance of a namespace."
End If
End Sub
Thus if you ever want to call Foo in this namespace you have to write
MyNamespace.Foo()
While simply calling Foo() will not work.
My question is this:
Is there any reason you wouldn't want to do this? As far as I can see the class' constructor is only called the first time you call any of its functions after you open your project, so I can't see any overhead there, but there could be something sneaky I am not aware of. Of course this is in no way an ideal way of addressing this lack of functionality, but it is not as if VBA programmers aren't already using the language in roundabout ways to provide lacking functionality.
Also, I see that this is basically the same as this question, however I'd further want to know whether there are any specific issues with having a ton of predeclared "empty" classes in your code. Say you replace 100 modules with 100 predeclared classes, will that have any significant impact on e.g. performance/stability/etc...?

It costs you an object pointer, is all. I wouldn't call it a namespace though.
Every single UserForm module you've ever worked with has a VB_PredeclaredId attribute set to True. This instance is automatically created, as you noticed, and while you can explicitly destroy it, the next time it's referenced it'll be automatically (silently) re-created again... with whatever the original state is/was... pretty much like an auto-instantiated object, aka an As New declaration.
So what you have there is more like an object hierarchy than a namespace structure - very, very similar to how you can drill down the Excel object model starting with Application, and go do this:
Set someCell = Excel.Application.Workbooks(1).Worksheets(1).Range("A1")
Here the "namespace" is the Excel library, Application is the root object, and the rest of that expression is all member access - property getters, named after classes.
But the classes aren't in "namespaces"... the Worksheets collection type (class) exists on its own, under the Excel library: there being a Worksheets property on the Application class does not in any way, shape, or form, shield the class from a "name collision": if your user code has a class module named Worksheets (and it very well can), then as per how VBA resolves identifier references, Dim foo As Worksheet is going to be an instance of that custom class: the only valid qualifier is the library name (Excel, or MyVBAProject).
So, what you have there is a reasonable approach for building a relatively complex object model structure - but it won't (can't) replace or simulate a namespace. If you find yourself making "empty" classes, reconsider your design.
As for the last question, I don't see how having 100 predeclared custom objects might be any different than a project with 100 userforms: in both cases I'd wonder if there'd be a way to trim that down and generalize/reuse components, whether they be forms or classes.
What you want to avoid, is a predeclared object that holds state - because that state is effectively global now, and you have no control over what gets to change that state, where, and when: Application.Calculation is a prime example of this.

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.

VBA subclass reference

In this post there is the description of "subclass" usage in VBA. I'm looking for the next step of it: when I have first subitem added I want to use it and don't know how.
When I write baseItem(1).itemName it doesn't work.
I assume it's just because that baseItem is not a collection or an array, but I don't know any other way.
Welcome to SO!
In vba like many other languages it is a Property. You call them directly and not numerically, but you need to write GET/LET methods unless the class is public (that is not really encapsulation to simply access directly).
baseItem.itemName would be your call.
But ... As I said before, better that you write accessor(s) to your class as methods.
Here is a guy that sets the tone for OOP in VBA (for me):
https://stackoverflow.com/a/45570268/8716187
He is a driver of the Rubberduck project.
I would ask you to ask yourself if you really need a class, I will often use 4-10 dictionaries of keys holding arrays. I could wrap them in a class but why bother? What I need is a searchable and editable ("array"-the dictionary of arrays).
I have very few class modules written, it seems that one can operate without it many times.
-WWC
I assume it's just because that baseItem is not a collection or an
array...
The baseItem itself is not a collection nor an array. It is just an object of type BaseClass. But this object baseItem wrapps a collection so we maybe could say it is almost a collection.
The problem with this object though is, that as it is defined now in the answer you mentioned, it provides no way how the clients can get to this collection. The class BaseClass needs to be modified so it provides access to this inner collection for the client of this class. With access e.g. some public function is meant which will return something from this collection.
Add something like this to the BaseClass:
Public Function getSubItem(index As Variant) As SubClass
Set getSubItem = subClassCollection.Item(index)
End Function
Now objects which will be at runtime created based on this class definition will provide access to the inner collection via this function getSubItem. The code which will use this function will look like this. So it is now almost that what you are trying to achieve.
Dim name As String
name = baseItem.getSubItem(1).itemName
Debug.Print name ' Prints "Something" in output window
But it could be made even exactly to what you are trying to achieve. When exporting the file of BaseClass.cls and adding Attribute Value.VB_UserMemId = 0 to the very beginning of function getSubItem and importing it again to project.
Public Function getSubItem(index As Variant) As SubClass
Attribute Value.VB_UserMemId = 0
Set getSubItem = subClassCollection.Item(index)
End Function
Now you can really write your code exactly that way you wanted. HTH
Dim name As String
name = baseItem(1).itemName
Debug.Print name ' Prints "Something" in output window
For more information about Creating A Default Member In VBA have a look e.g. here.

Excel add-in with logging class: How to react to VBA state loss?

The setup
I'm developing and maintaining an Excel add-in that comes with its own tab of controls within Excel's Ribbon UI. I've come across the problem of state loss before (meaning loss of all variables with global scope, static variables, etc, which of course includes my reference to the RibbonUI). With regards to the ribbon reference I've "solved" the problem by including a "Reset Ribbon" button that restores the reference from a persistently stored pointer and then invalidates the ribbon. Although certainly not the most elegant, this part works just fine.
However, after the introduction of a logging class, the state loss issue haunts me once again. The logger is instantiated in ThisWorkbook's module:
Private Sub Workbook_Open()
Set LogToFile = SingletonFactory.getToFileLogger
End Sub
and is then put to work, for example, as follows:
Private Sub buttonReloadObjects_onAction(ByVal control As IRibbonControl)
LogToFile.trace "Event firing: buttonReloadObjects_onAction"
' more stuff happening...
invalidateRibbon ' restores ribbon object and invalidates it
End Sub
The logger is instantiated when the add-in is loaded so that I have the freedom to log whatever I want within the confines of my add-in's code. It has several logging levels like trace/debug/error/... and a couple of other methods. Usually it works just fine - until the state loss hits (usually caused by an unrelated error, followed by clicking "End").
State loss
At this point the VBA environment forgets about the very existence of my LogToFile object and nothing works any more, because every click on the ribbon controls will trigger a runtime error 91: Object variable or with block variable not set pointing to whatever line is the first to contain a reference to LogToFile.
A solution?
Now, short of doing crazy workarounds like placing
if not isObject(LogToFile) then
Set LogToFile = SingletonFactory.getToFileLogger
end if
LogToFile.trace "Message"
before any occurrence of LogToFile, the only real "solution" I was able to come up with is to wrap all my logger calls in functions (residing in a standard module) and call these functions any time I want to send something to the log. This way I could catch the missing object reference right before the object is needed and I avoid calling methods of uninstantiated objects.
However, after having everything neatly encapsulated in class modules, it strikes me as odd, maybe even wrong(?), going down this route.
So, is there a "proper" solution to the problem of a lost logger instance? Or is my suggested approach already as proper as it can get?
Note: This problem is of course not specific to logging classes. It affects all global variables, most notably my ApplicationEventClass. The issue just happens to be the most glaring with the logger due to its frequent usage around all entry points to the code.
You only need one function that either returns the original variable or resets it. If you call that function LogToFile you don't need to change any of the other code other than removing the Workbook_Open code which is then superfluous. So:
Function LogToFile() As WhateverVariableType
Static temp as WhateverVariableType
If temp is Nothing then Set temp = SingletonFactory.getToFileLogger
Set LogToFile = temp
End Function
This way you will also still benefit from Intellisense when writing the code.
Note: you may not actually need the temp variable - it depends on whether there are settings that you want persisted. If there are, you may want to reset them in the function too.

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