VBA Reference counting - Object destruction - excel

Lately I've bumped into a question that made me pounder; it kept me busy and I couldn't find a transparent explanation for it on the net.
It is related to the destruction of Excel objects (which I use all the time and never really questioned before).
Background leading to my question:
With regular objects, you can instantiate an object using the keywords SET and NEW.
For example:
Set classInstance = New className
Whenever we instantiate this way, the object is created in the heap memory and the reference counter is increased by 1.
In case I don't add more references, the following statement would bring the reference count back to zero:
Set classInstance = Nothing
When the reference count goes to 0, the object is destroyed and cleared from memory and the "classInstance" points to .
What I've read:
When we use the "CREATEOBJECT" function, it returns a reference to a COM object.
Set oApp = CreateObject("Excel.Application")
Even though we could say:
Set oApp = nothing
The objects' reference count will go to 0, and oApp will not point to the object anymore.
My questions:
1) Why is it that this type of object requires to call the method .Quit before the object is actually being removed from memory?
The same goes when adding a reference to a workbook object (workbooks.add or workbook.open) which requires the .close method.
Why can't these objects be automatically destroyed when bringing the reference count to zero?
Which is the case when we say for example:
set oRange = nothing
2) And is there a need to say:
oApp.Quit
set oApp = nothing
Since the Application object is already cleared from memory when applying .Quit, there is no object to be released anymore.
The only reason I could come up with, why oApp would be set to Nothing after Quit, would be because it could be pointing to an unused memory location (on the heap) and could lead to confusion later if this memory would be re-assigned (although in VBA I find this hard to imagine).
I was questioning myself if this conclusion is correct and I would like to receive confirmation for that from someone who knows the answer.
Please, tell me if I see this wrongly.
3) What they call in VBA "a reference to an object" (such as oApp in the code above), I see them as pointer variables in C.
Would it be safe to use this statement or again, am I seeing this wrongly?
Generally is not hard to apply .Quit and set to nothing, but it would be nice to receive some accurate information on the topic. So that I know for 100% percent why I am doing it.

Good Question :)
Excel controls the creation of its objects. Likewise it also controls their destruction.
Setting oApp = Nothing just destroys the object reference. It doesn't remove the Application. To destroy an Excel object, you have to use it's .Quit method.
Whenever you do, Set x = Nothing, the reference(pointer) named x to its relevant object is removed. This doesn't mean that the object itself will be removed from the memory.
Whether the object will be removed from memory or not, depends on various factors.
Whether there are more references pointing towards the same object. If there are, the object will not be removed. The reference count must be zero.
The internal implementation of the destructor of that object.
The .Quit method is defined to graciously remove all the memory objects excel has allocated, and close itself.
It is similar to calling Close on a form in VB6. Take for example, a form in vb6.
Dim f As Form
Set f = Form1
f.Show
'
'~~> Rest of the code
'
Set f = Nothing
Will this destroy the form? :)
FOLLOWUP
How about question 2? Thanks – Kim Gysen 14 mins ago
It might not be exactly as shown here, and compiler optimizations may make things behave differently... but this is the basic concept that is at work.

Part 2 of your question is quite interesting, and it's well worth an extended answer.
This is going to cover three key points:Objects and object variables;Pitfalls when dismissing objects;...And an important change in reference-counting the Application object in Excel 2013.
But, if you want a short answer, it's: "Not all objects are equal".
Now read on...
Some objects are created in your Excel session's 'own' memory space, and their memory allocation is controlled by your session; some objects have persistent components that exist after the object variable is dismissed; and some do not:
Set oDict = CreateObject("Scripting.Dictionary")
Set oWShell = CreateObject("Shell.Application")
In both these cases, memory is allocated, the object variables (and their vTable of pointers to methods and properties) are instantiated, and they are yours to command until you dismiss them:
Set oDict = Nothing
Set oWShell = Nothing
And, on dismissal, no trace of them remains.
But this object is persistent:
Dim oWbk as Excel.Workbook
Set oWbk = Application.Workbooks.Add
...You've created a new workbook object and, if you dismiss the object variable with Set oWbk = Nothing, you will see that the new workbook object still exists as a visible presence in the user interface.
What you actually created was a Workbook object - a workbook window with an active worksheet and the full user interface that goes with that - and a Workbook object variable - a programmer's COM interface, a table of methods and properties for the Workbook object - that you can manipulate in code using the named entity 'oWbk'.
Dismissing the oWbk object variable removes that framework, but the Workbook itself will still exist: you've created a Workbook object, and it's yours to keep.
The object is more than its object variable and dismissing the variable does not destroy the object: it just dismisses an interface, a framework of methods and properties that you can use to manipulate the object in code.
Closing the Workbook, with or without saving a file, should automatically dismiss the object variable and clear up the memory allocated for that interface of properties, methods and attributes:
'try this:
oWbk.Close SaveChanges:=False
' or maybe this:
Application.Workbooks(Application.Workbooks.Count).Close SaveChanges:=False
...That is to say, you would expect both of those commands to call Set oWbk= Nothing - especially the oWbk.Close command - but if you try either of them without explicitly dismissing oWbk, you will find that oWbk still exists as an empty husk, and all calls and requests for information on it (try> Debug.Print> TypeName(oWbk) ) will return 'Automation Error' messages.
Some of the commments in the previous answer mention the UserForm object which - unlike the Dictionary and the Shell object - is an object with a visible user interface. But this user interface is not a persistent new object in the Excel user interface like a Workbook or a worksheet.
Luckily for you, the object you created is owned by your Excel session, and you can instantiate an object variable again, to get the same framework of methods and properties, and take control of the object again:
Set oWbk = Application.Workbooks(Application.Workbooks.Count)
...Assuming, of course, that you have some way of being sure that you identified the right workbook object: but that's not your question at all.
Where this answer is going is: objects that are not created in your Excel session's 'own' memory.
Set oApp = CreateObject("Excel.Application")
This statement will create an Excel object which, like the new Workbook, has a User Interface (although you'll need to set the .Visible property True to see it) and and a persistent presence in memory: once again, the object is more than its object variable, and dismising the variable does not destroy the object.
Unlike the new Workbook, it isn't quite yours to command: it's an Excel session in it's own right, it allocates its own memory - oApp's 'footprint' in your current session's memory is just the pointer and the name: the interface (vTable, iDispatch, and all those named methods with pointers to the structures that implement the arcane act of manipulating an Excel session in VBA) exists in the block of memory allocated by this new Excel session.
Here's what happens in Office 2010, and older versions of Excel:
Dismissing the object variable with Set oApp = Nothing leaves that session up and running, and I would strongly suggest that you make the session visible so that you can close it manually!
Closing that Excel session manually, without explicitly dismissing the oApp object variable, will definitely leave oApp in the 'empty husk' state, and a grim and headless spectre wailing 'The Automation object has disconnected from its clients!' in the dark corners of your code base.
But, in Office 2013 and later versions, Set oApp = Nothing performs exactly the reference-counting you would expect and the session closes. Try it:
Private Sub Test()
Dim oApp As Excel.Application
Set oApp = New Excel.Application
'Set oApp = CreateObject("Excel.Application")
oApp.Visible = True
Set oApp = Nothing
End Sub
It won't close on Set oApp = Nothing if another object variable has a reference - and that's not the only entity that gets to increment the reference counter: user activity in the GUI (try creating a new workbook and editing it) keeps the session up and running, too.
For your own amusement, see if oApp.Quit does actually dismiss oApp and sets it to Nothing.
Of course, oApp.Quit will definitely close the session...
...Or will it? If there is something going on in that session - a long calculation, or an 'modal' error message that you have to see and click before the Application object responds to any other input, from the user interface or your VBA - then oApp.Quit won't close the session.
Lets not go there. All things being equal, oApp.Quit will definitely close the session in 2010 and earlier versions of Excel.
But in Office 2013, calling 'Quit' from the last object variable merely hides the UI: the object variable still responds to your code - the methods and properties that don't require an active workbook are still accessible via oApp - and a separate instance of Excel.exe is clearly visible in the Processes tab of Task manager.
Likewise, quitting the new session by clicking the 'close' button in the user interface closes the session's windows but, if there's an object variable with a reference to this application object in your code, it's still there, in memory, and 'oApp' can still get at the properties and methods.
So the reference counter works both ways in current versions of Excel: the object exists until the reference count decrements to zero, and the last remaining object variable will not be left 'disconnected' by a quit command or UI action.
Nevertheless, your session doesn't 'own' that new application object: if you've dismissed the last object variable and set it to Nothing, and there's something else keeping the neww session alive - user activity, or some internal process - there's nothing like the Application.Workbooks() or Worksheets() collection to identify other Excel sessions and instantiate an object variable pointing to a specific instance of an Excel.Application object.
There are ways of getting a specific session using API calls, but they aren't as reliable as you might wish them to be.
...So, all in all, there's quite a lot in that 'part 2'.

Related

Confusion in creating excel application object using VB.net: CreateObject vs New

I am facing difficulty in understanding the following concepts. I had posted a question some time back - read through the answers but some things are still not clear. I state my confusion below:
My first question refers to the following code piece
Option Strict On
Imports Microsoft.Office.Interop
Dim oxl As Excel.Application
oxl = CreateObject("Excel.Application")
In the above code piece, the statement oxl = CreateObject("Excel.Application") throws an error stating, Option Strict On disallows implicit conversions from Object to Application. My question is I read from many sources that it is always better to keep Option Strict ON but in this case when we need to create a new excel application, the Option Strict ON is preventing us from doing so. So what is the best practice that should be followed for such a conflict?
Next I tried replacing the statement oxl = CreateObject("Excel.Application") with oxl = New Excel.Application. It was observed that even with Option Strict ON, we can create a new excel application object with the NEW keyword. It was also checked with GetType that in both cases that is, using CreateObject and NEW, the type of object being created was: System._ComObject.So my question is if the type of object being created remains remains the same, why is that Option Strict disallows CreateObject but allows the creation of the excel application object using NEW?
To study it further, I extended the above code to the following:
Option Strict On
Imports System
Imports Microsoft.Office.Interop
Module Program
Dim oxl As Excel.Application
Dim owb As Excel.Workbook
Dim osheet As Excel.Worksheet
Sub Main()
oxl = New Excel.Application
'oxl = CreateObject("Excel.Application")
Console.WriteLine(oxl.GetType)
oxl.Visible = True
owb = oxl.Workbooks.Add()
osheet = owb.Worksheets("Sheet1") ‘Error: Option Strict ON disallows implicit conversions from ‘Object’ to ‘Worksheet’
osheet.Range("A1").Value = 53
Console.WriteLine("Hello World!")
Console.ReadLine()
End Sub
End Module
When we run the code we see that the error Option Strict ON disallows implicit conversions from ‘Object’ to ‘Worksheet’ comes at the line: osheet = owb.Worksheets("Sheet1")
Question:
Why is the error coming? I mean if, owb = oxl.Workbooks.Add()can work (that it returns a workbook which is referred to by owb) then why is osheet = owb.Worksheets("Sheet1") not working because the right hand side returns the “Sheet1” of the workbook which osheet should be able to point to (given that it is of the type Excel.Worksheet)?
Sometimes it just doesn't have the information that something is more specific than an Object. If you don't use the default property and use Item instead, as in owb.Worksheets.Item("Sheet1"), you can hover over the Worksheets part to see that represents the .Sheets, but hovering over the Item part reveals it has no details of the items therein - it says it returns an Object.
You know what it should be, so if you had
Imports XL = Microsoft.Office.Interop.Excel
then you could do
osheet = DirectCast(owb.Worksheets("Sheet1"), XL.Worksheet)
and the types would all work out.
This is what VB statements about COM objects actually do.
Information for Visual Basic Programmers
Visual Basic provides full support for Automation. The following table
lists how Visual Basic statements translate into OLE APIs.
Visual Basic statement OLE APIs
CreateObject (“ProgID”)
CLSIDFromProgID
CoCreateInstance
QueryInterface to get IDispatch interface.
GetObject (“filename”, “ProgID”)
CLSIDFromProgID
CoCreateInstance
QueryInterface for IPersistFile interface.
Load on IPersistFile interface.
QueryInterface to get IDispatch interface.
GetObject (“filename”)
CreateBindCtx creates the bind context for the subsequent functions.
MkParseDisplayName returns a moniker handle for BindMoniker.
BindMoniker returns a pointer to the IDispatch interface.
Release on moniker handle.
Release on context.
GetObject (“ProgID”)
CLSIDFromProgID
GetActiveObject on class ID.
QueryInterface to get IDispatch interface.
Dim x As New interface
Find CLSID for interface.
CoCreateInstance
QueryInterface
A standard COM VTable the first three entries are IUnknown
AddRef, Release (decreases the ref count), and QueryInterface to find what interfaces this object support.
The next four entries are IDispatch
GetIDsOfNames, Invoke , GetTypeInfoCount, GetTypeInfo.
The entries after that are your methods and properties, and all are a indirect function call.
To get the code in memory you use the COM API calls, such as CoCreateInstance
You need to decide to early or late bind. Early binding requires the program to be installed so its type library can be read so it types are compiled into the program. Late binding doesn't care about compile time. There is a conversation Hello object, do you have a function called x. Object replies Yes, it is function 7, Can you please do function 7 object. Early binding function 7 is hard coded. You can only late bind to generic objects. –
So a COM object is 4 x 32 bit. One is the reference count, one is the address if the Virtual Function Table (VTable), 2 are unused. In early binding to call a function 7 the compiler does Address_Of_Vtable + (4 x 7) (being 4 bytes for an address). See IDispatch. N.B. Microsoft.Office.Interop is not used at all in late binding. –
Only the generic object can be used in late binding and cannot be used in early binding. Early binding requires you to tell it the specific object. You are mixing and matching. The compiler is confused, just like you. –
Option Strict Restricts implicit data type conversions to only widening conversions, disallows late binding, and disallows implicit typing that results in an Object type. learn.microsoft.com/en-us/dotnet/visual-basic/… –

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.

Getting 'Invalid use of property' when using Property Set in VBA

I know there are tons of threads and questions about this and it's pretty obvious, generally, where the error is. Most folks aren't using the SET keyword when moving objects around. I am.
Here's what's happening:
This is in an excel sheet so I've made a little set of functions to keep track of the columns and make an index so that each time the app runs it will reindex the columns so I can do things like .Cells(row_num, pCust_index("custID")) in case of column changes.
I have a form called custContagions. It's just a little modal window that allows users to add/remove/edit customer's contagious status. It contains a property:
Private pCust_index as dictionary
It also contains this property setter:
Public Property Set set_cust_index(ByRef custIndex As Dictionary)
Set pCust_index = New Dictionary
Set pcust_index = custIndex
End Property
Pretty straight forward right? Takes a dictionary object, resets my index and points to the existing passed object.
Now, on the calling form I have the other side:
Private Sub newCustContagious_LBL_Click()
Dim contForm as New custContagions
Call contForm.set_cust_index(pCust_index) 'note pCust_index is a private here too
Call contForm.Show
...
I'm getting the Invalid Use of Property compiler error on the set_cust_index call.
What did I miss?
Most folks aren't using the SET keyword when moving objects around
Then they are not moving objects around. The Set keyword is the way to move object around.
There is also CopyMemory to directly copy the ObjPtr, but I do not believe most folks do that.
Pretty straight forward right?
Not quite. You create a dictionary, immediately discard it and replace with another dictionary passed as a parameter. You should remove the first of the two lines, and make the param ByVal:
Public Property Set set_cust_index(ByVal custIndex As Dictionary)
Set pcust_index = custIndex
End Property
I'm getting the Invalid Use of Property compiler error
You declared a property and then use it as a sub. With a property, you should have done:
Set contForm.set_cust_index = pCust_index
At which point the set_cust_index name does not look great. It would make a sensible name for a sub (Public Sub set_cust_index(ByVal custIndex As Dictionary)), but for a property you would be better off with Public Property Set cust_index(ByVal custIndex As Dictionary).

Resources