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

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.

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.

Using predeclared classes in VBA as namespaces

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.

Is there a way in lotus notes to have a global (to a document) error handler?

I've inherited a lotus notes application and one of the things that really irks me is every function/sub/property has onerror statements and errorhandler labels that aside from typos all do the exact same thing. Additionally, and unfortunately this application has gone through several revisions and some errorhandler: labels have revisions where as other don't. I'd like to standardize and centralize this behavior.
Is there a way to have a single error handler for a given document, where if an error is raised anywhere in the document, that particular error handler is called?
Thank you,
You can have one error handler per script execution. You cannot have one global to a document. Each event that fires in a document results in a new script execution.
That said, it is generally advantageous to have one error handler per function, but that advantage is lost if they are actually exactly the same. The better practice is to customize them so that each error handler records the name of the current function. (Of course, due to copy/paste laziness, this is frequently more effective in theory than in practice.)
If you have an On Error Goto SomeLabel statement (where SomeLabel is whatever label the code actually uses), the label must exist in the same Sub/Function that contains that statement so, technically, you need a separate handler for each Sub/Function.
However, some things might simplify matters...
If one Sub/Function calls another Sub/Function, and the inner one doesn't have an error handler but the outer one (the caller) does, then an error in the inner Sub/Function will be caught by the handler in the caller.
This setup gives you less information (you can't get the line number on which the error occurred in the inner Sub/Function), but it might be helpful if you have any Subs/Functions that you're sure either can't produce an error, or only have one line on which an error could occur.
If you have some static message-text or logging which is identical in many error handlers, you could have a Sub/Function in the Form Globals (or in a script library to absolutely minimise code duplication) that contains the static parts of the error handlers, and takes arguments for the variable parts (error message, line number, and Sub/Function name).
Finally, this code will produce the name of the current Sub/Function and makes it easier to use the same error handler in many places, as long as the code declarations contain %include "lsconst.lss" or you use a script library containing the same %include statement:
GetThreadInfo(LSI_THREAD_PROC)
Another function, LSI_Info, can also give you the name of the current Sub/Function, but isn't supported by IBM, and should be avoided.

"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

Resources