I'm using VBA for Excel. From my understanding, global variables need to be declared outside of any subs. That's the only way they can be accessed by all subs.
At the meantime, I want to do late binding to reference the "Microsoft Scripting Runtime" library(in order to use the dictionary object type) so that an end user doesn't have to do it himself.
My code is as below:
On Error Resume Next
strGUID = "{420B2830-E718-11CF-893D-00A0C9054228}"
ThisWorkbook.VBProject.References.AddFromGuid GUID:=strGUID, Major:=1, Minor:=0
Dim Dic1 As Object
Set Dic1 = CreateObject("Scripting.Dictionary")
Dim Dic2 As Object
Set Dic2 = CreateObject("Scripting.Dictionary")
What if I want to declare global dictionary object with late binding? It looks like VBA won't allow me to put any code outside of the subs (other than the declarations).
How may I declare a global dictionary object without needing the end user configure library reference himself? Shall I don the following?
Dim Dic1 As Object
Dim Dic2 As Object
Sub Prog1()
On Error Resume Next
strGUID = "{420B2830-E718-11CF-893D-00A0C9054228}"
ThisWorkbook.VBProject.References.AddFromGuid GUID:=strGUID, Major:=1, Minor:=0
Set Dic1 = CreateObject("Scripting.Dictionary")
Set Dic2 = CreateObject("Scripting.Dictionary")
End Sub
Like the VBA code itself, project references don't magically disappear when your user opens your host workbook. They're saved along with the code in the host document.
So, the premise of your question is wrong: users never need to tweak project references.
Also the Scripting Runtime type library is standard issue and has been shipped the exact same version on every single Windows machine built this century (even before that), which means unless your code needs to run on a Mac, there's no need to ever late-bind the Scripting Runtime library.
And if your code needs to run on a Mac, the library won't late-bind anyway because it won't be found on the host machine, so late-binding the Scripting Runtime only serves to make silly typos and introduce other easily avoidable bugs that IntelliSense helps preventing.
ThisWorkbook.VBProject.References.AddFromGuid GUID:=strGUID, Major:=1, Minor:=0
This defeats the entire purpose of late-binding: it's using the VBIDE extensibility library (which requires lowered macro security settings) to programmatically add a reference that you can easily add at design-time through the VBE's Tools menu.
Late-bound code doesn't need the reference at all. Not at compile-time, not at run-time.
Add the reference, save, then declare your objects As Scripting.Dictionary and enjoy the benefits of early-bound code.
Set Dic1 = New Scripting.Dictionary
That's all you need.
What if I want to declare global dictionary object with late binding? It looks like VBA won't allow me to put any code outside of the subs (other than the declarations).
Late binding isn't any different than early binding in that aspect. The only difference between late and early bound code is the As clause of the declaration:
Private foo As Object ' no compile-time type knowledge: late-bound
Private bar As Dictionary ' compile-time type knowledge: early-bound
How you're initializing that object reference makes no difference to the late/early binding nature of the declaration.
This looks up a ProgID in the registry to find the library and the type:
Set foo = CreateObject("Scripting.Dictionary")
This uses the project references:
Set foo = New Scripting.Dictionary
Both are correct, and both will work against either early or late-bound declarations. Except, if you already have a reference to the type library, there's not really a need to go hit the registry to locate that library - just New it up!
Global variables are really not needed and should be avoided. However, if you have decided to use them for a your own reasons, you can put them in the Workbook_Open event:
Option Explicit
Dim Dic1 As Object
Dim Dic2 As Object
Private Sub Workbook_Open()
Set Dic1 = CreateObject("Scripting.Dictionary")
Set Dic2 = CreateObject("Scripting.Dictionary")
End Sub
Thus, it would create the object every time the workbook is opened.
Related
I have written a version control module. The AutoExec macro launches it whenever I, or one of the other maintainers log in. It looks for database objects that have been created or modified since the previous update, and then adds an entry to the Versions table and then opens the table (filtered to the last record) so I can type in a summary of the changes I performed.
It is working great for Tables, Queries, Forms, Macros, etc but I cannot get it to work correctly for modules.
I have found two different properties that suggest a Last Modified date ...
CurrentDB.Containers("Modules").Documents("MyModule").Properties("LastUpdated").Value
CurrentProject.AllModules("MyModule").DateModified
The first one (CurrentDB) always shows "LastUpdated" as the Date it was created, unless you modify the description of the module or something in the interface. This tells me that this property is purely for the container object - not what's in it.
The second one works a lot better. It accurately shows the date when I modify and compile/save the module. The only problem is that when you save or compile a module, it saves / compiles ALL the modules again, and therefore sets the DateModified field to the same date across the board. It kind of defeats the purpose of having the DateModified property on the individual modules doesn't it?
So my next course of action is going to a bit more drastic. I am thinking I will need to maintain a list of all the modules, and count the lines of code in each module using VBA Extensions. Then, if the lines of code differs from what the list has recorded - then I know that the module has been modified - I just won't know when, other than "since the last time I checked"
Does anyone have a better approach? I'd rather not do my next course of action because I can see it noticeably affecting database performance (in the bad kind of way)
Here's a simpler suggestion:
Calculate the MD5 hash for each module.
Store it in the Versions table.
Recalculate it for each module during the AutoExec and compare it to the one in the Versions table. If it's different, you can assume it has been changed (while MD5 is bad for security, it's still solid for integrity).
To get the text from a module using VBE Extensibility, you can do
Dim oMod As CodeModule
Dim strMod As String
Set oMod = VBE.ActiveVBProject.VBComponents(1).CodeModule
strMod = oMod.Lines(1, oMod.CountOfLines)
And then you can use the following modified MD5 hash function from this answer as below, you can take the hash of each module to store it, then compare it in your AutoExec.
Public Function StringToMD5Hex(s As String) As String
Dim enc
Dim bytes() As Byte
Dim outstr As String
Dim pos As Integer
Set enc = CreateObject("System.Security.Cryptography.MD5CryptoServiceProvider")
'Convert the string to a byte array and hash it
bytes = StrConv(s, vbFromUnicode)
bytes = enc.ComputeHash_2((bytes))
'Convert the byte array to a hex string
For pos = 0 To UBound(bytes)
outstr = outstr & LCase(Right("0" & Hex(bytes(pos)), 2))
Next
StringToMD5Hex = outstr
Set enc = Nothing
End Function
You can't know when a module was modified. The VBIDE API doesn't even tell you whether a module was modified, so you have to figure that out yourself.
The VBIDE API makes it excruciatingly painful - as you've noticed.
Rubberduck doesn't deal with host-specific components yet (e.g. tables, queries, etc.), but its parser does a pretty good job at telling whether a module was modified since the last parse.
"Modified since last time I checked" is really all you need to know. You can't rely on line counts though, because this:
Option Explicit
Sub DoSomething
'todo: implement
End Sub
Would be the same as this:
Option Explicit
Sub DoSomething
DoSomethingElse 42
End Sub
And obviously you'd want that change to be picked up and tracked. Comparing every character on every single line of code would work, but there's a much faster way.
The general idea is to grab a CodeModule's contents, hash it, and then compare against the previous content hash - if anything was modified, we're looking at a "dirty" module. It's C#, and I don't know if there's a COM library that can readily hash a string from VBA, but worst-case you could compile a little utility DLL in .NET that exposes a COM-visible function that takes a String and returns a hash for it, shouldn't be too complicated.
Here's the relevant code from Rubberduck.VBEditor.SafeComWrappers.VBA.CodeModule, if it's any help:
private string _previousContentHash;
public string ContentHash()
{
using (var hash = new SHA256Managed())
using (var stream = Content().ToStream())
{
return _previousContentHash = new string(Encoding.Unicode.GetChars(hash.ComputeHash(stream)));
}
}
public string Content()
{
return Target.CountOfLines == 0 ? string.Empty : GetLines(1, CountOfLines);
}
public string GetLines(Selection selection)
{
return GetLines(selection.StartLine, selection.LineCount);
}
public string GetLines(int startLine, int count)
{
return Target.get_Lines(startLine, count);
}
Here Target is a Microsoft.Vbe.Interop.CodeModule object - if you're in VBA land then that's simply a CodeModule, from the VBA Extensibility library; something like this:
Public Function IsModified(ByVal target As CodeModule, ByVal previousHash As String) As Boolean
Dim content As String
If target.CountOfLines = 0 Then
content = vbNullString
Else
content = target.GetLines(1, target.CountOfLines)
End If
Dim hash As String
hash = MyHashingLibrary.MyHashingFunction(content)
IsModified = (hash <> previousHash)
End Function
So yeah, your "drastic" solution is pretty much the only reliable way to go about it. Few things to keep in mind:
"Keeping a list of all modules" will work, but if you only store module names, and a module was renamed, your cache is stale and you need a way to invalidate it.
If you store the ObjPtr of each module object rather than their names, I'm not sure if it's reliable in VBA, but I can tell you that through COM interop, a COM object's hashcode isn't going to be consistently consistent between calls - so you'll have a stale cache and a way to invalidate it, that way too. Possibly not an issue with a 100% VBA solution though.
I'd go with a Dictionary that stores the modules' object pointer as a key, and their content hash as a value.
That said as the administrator of the Rubberduck project, I'd much rather see you join us and help us integrate full-featured source control (i.e. with host-specific features) directly into the VBE =)
I thought I would add the final code I came up with for a hash / checksum generation module, since that was really the piece I was missing. Credit to the #BlackHawk answer for filling in the gap by showing that you can late bind .NET classes - that's going to open up a lot of possibilities for me now.
I have finished writing my Version checker. There were a few caveats that I encountered that made it hard to rely on the LastUpdated date.
Resizing the columns in a Table or Query changed the LastUpdated date.
Compiling any Module compiled all modules, thus updated all module's LastUpdated date (as was already pointed out)
Adding a filter to a form in View mode causes the form's Filter field to be updated,which in turn updates the LastUpdated date.
When using SaveAsText on a Form or Report, changing a printer or display driver can affect the PrtDevMode encodings, so it is necessary to strip them out before calculating a checksum
For Tables I built a string that was a concatenation of the table name, all field names with their size and data types. I then computed the hash on those.
For Queries I simply computed the hash on the SQL.
For Modules, Macros, Forms, and Reports I used the Application.SaveAsText to save it to a temporary file. I then read that file in to a string and computed a hash on it. For Forms and Reports I didn't start adding to the string until the "Begin" line passed.
Seems to be working now and I haven't come across any situations where it would prompt for a version revision when something wasn't actually changed.
For calculating a checksum or hash, I built a Class Module named CryptoHash. Here is the full source below. I optimized the Bytes Array to Hex String conversion to be quicker.
Option Compare Database
Option Explicit
Private objProvider As Object ' Late Bound object variable for MD5 Provider
Private objEncoder As Object ' Late Bound object variable for Text Encoder
Private strArrHex(255) As String ' Hexadecimal lookup table array
Public Enum hashServiceProviders
MD5
SHA1
SHA256
SHA384
SHA512
End Enum
Private Sub Class_Initialize()
Const C_HEX = "0123456789ABCDEF"
Dim intIdx As Integer ' Our Array Index Iteration variable
' Instantiate our two .NET class objects
Set objEncoder = CreateObject("System.Text.UTF8Encoding")
Set objProvider = CreateObject("System.Security.Cryptography.MD5CryptoServiceProvider")
' Initialize our Lookup Table (array)
For intIdx = 0 To 255
' A byte is represented within two hexadecimal digits.
' When divided by 16, the whole number is the first hex character
' the remainder is the second hex character
' Populate our Lookup table (array)
strArrHex(intIdx) = Mid(C_HEX, (intIdx \ 16) + 1, 1) & Mid(C_HEX, (intIdx Mod 16) + 1, 1)
Next
End Sub
Private Sub Class_Terminate()
' Explicity remove the references to our objects so Access can free memory
Set objProvider = Nothing
Set objEncoder = Nothing
End Sub
Public Property Let Provider(NewProvider As hashServiceProviders)
' Switch our Cryptographic hash provider
Select Case NewProvider
Case MD5:
Set objProvider = CreateObject("System.Security.Cryptography.MD5CryptoServiceProvider")
Case SHA1:
Set objProvider = CreateObject("System.Security.Cryptography.SHA1CryptoServiceProvider")
Case SHA256:
Set objProvider = CreateObject("System.Security.Cryptography.SHA256Managed")
Case SHA384:
Set objProvider = CreateObject("System.Security.Cryptography.SHA384Managed")
Case SHA512:
Set objProvider = CreateObject("System.Security.Cryptography.SHA512Managed")
Case Else:
Err.Raise vbObjectError + 2029, "CryptoHash::Provider", "Invalid Provider Specified"
End Select
End Property
' Converts an array of bytes into a hexadecimal string
Private Function Hash_BytesToHex(bytArr() As Byte) As String
Dim lngArrayUBound As Long ' The Upper Bound limit of our byte array
Dim intIdx As Long ' Our Array Index Iteration variable
' Not sure if VBA re-evaluates the loop terminator with every iteration or not
' When speed matters, I usually put it in its own variable just to be safe
lngArrayUBound = UBound(bytArr)
' For each element in our byte array, add a character to the return value
For intIdx = 0 To lngArrayUBound
Hash_BytesToHex = Hash_BytesToHex & strArrHex(bytArr(intIdx))
Next
End Function
' Computes a Hash on the supplied string
Public Function Compute(SourceString As String) As String
Dim BytArrData() As Byte ' Byte Array produced from our SourceString
Dim BytArrHash() As Byte ' Byte Array returned from our MD5 Provider
' Note:
' Because some languages (including VBA) do not support method overloading,
' the COM system uses "name mangling" in order to allow the proper method
' to be called. This name mangling appends a number at the end of the function.
' You can check the MSDN documentation to see how many overloaded variations exist
' Convert our Source String into an array of bytes.
BytArrData = objEncoder.GetBytes_4(SourceString)
' Compute the MD5 hash and store in an array of bytes
BytArrHash = objProvider.ComputeHash_2(BytArrData)
' Convert our Bytes into a hexadecimal representation
Compute = Hash_BytesToHex(BytArrHash)
' Free up our dynamic array memory
Erase BytArrData
Erase BytArrHash
End Function
I have read countless times that you should always release objects at the end of your projects, such as:
Sub Test()
Dim obj As Object
Set obj = GetObject(, "xxxxxx.Application")
' Code your project...
Set obj = Nothing
End Sub
However, I use a program that calls upon Excel's object. I call Excel's object so often in so many different routines, I decided to make a function to make things simpler:
Public Function appXL As Object
Set appXL = GetObject(, "Excel.Application")
End Function
Although that is an extremely simple function, there is no way for me to release the object appXL at the end of any subroutine I use, at least as far as I am aware. But a bigger concern that I have is that a subroutine that uses appXL 100+ times is actually grabbing the object the same number of times.
I honestly don't see why it would be a big issue since I am using GetObject as opposed to CreateObject. I am not creating a new instance of Excel everytime the function is called, so is this something that I should be concerned about when coding? Do I just need to completely get rid of the function and declare appXL on every routine I use so I am able to release it in the end?
I've created a custom class called BitArray. One of BitArray's methods, Append, takes a BitArray as an argument. Here is the declaration for the Append method:
Public Function Append(ByVal newBits As BitArray)
(I've also tried passing newBits ByRef to no effect)
In my code, I have something like this:
Dim baBits1 As BitArray
Dim baBits2 As BitArray
Set baBits1 = New BitArray
Set baBits2 = New BitArray
baBits1.Append (baBits2)
The last line throws the 438 error. Oddly enough, if I pull the code for Append out and execute it directly, it works. So there is nothing inherently wrong with the logic or syntax within Append. Does VBA not like class methods that can accept its own class as an argument?
VBA is fine with class methods that accept own class as an argument.
ByVal and ByRef makes no difference when passing object variables - they will be always be passed ByRef regardless.
Try setting a breakpoint then using F8 to step through the code line by line to see where the error is.
While perusing an application that I'm documenting, I've run across some examples of bang notation in accessing object properties/methods, etc. and in other places they use dot notation for what seems like the same purpose.
Is there a difference or preference to using one or the other? Some simple googling only reveals limited information on the subject with some people actually using it in opposite cases. Perhaps there is a coding standards section from MS somewhere that indicates the method of madness?
Despite the (formerly) accepted answer to this question, the bang is not in fact a member or collection access operator. It does one simple and specific thing: The bang operator provides late-bound access to the default member of an object, by passing the literal name following the bang operator as a string argument to that default member.
That's it. The object doesn't have to be a collection. It doesn't have to have a method or property called Item. All it needs is a Property Get or Function which can accept a string as the first argument.
For much more detail and proof, see my blog post discussing this: The Bang! (Exclamation Operator) in VBA
The bang operator (!) is shorthand for accessing members of a Collection or other enumerable object, such as the Fields property of an ADODB.Recordset.
For example, you can create a Collection and add a few keyed items to it:
Dim coll As Collection
Set coll = New Collection
coll.Add "First Item", "Item1"
coll.Add "Second Item", "Item2"
coll.Add "Third Item", "Item3"
You can access an item in this collection by its key in three ways:
coll.Item("Item2")
This is the most explicit form.
coll("Item2")
This works because Item is the default method of the Collection class, so you can omit it.
coll!Item2
This is short-hand for both of the above forms. At run-time, VB6 takes the text after the bang and passes it as a parameter to the Item method.
People seem to make this more complicated than it should be, which is why it's hard to find a straightforward explanation. Usually the complications or "reasons not to use the bang operator" stem from a misunderstanding of how simple it actually is. When someone has a problem with the bang operator, they tend to blame it instead of the real cause of the problem they are having, which is often more subtle.
For example, some people recommend not using the bang operator to access controls on a form. Thus, Me.txtPhone is preferred over Me!txtPhone. The "reason" this is seen as bad is that Me.txtPhone will be checked at compile-time for correctness, but Me!txtPhone won't.
In the first case, if you mistype the code as Me.txtFone and there is no control with that name, your code won't compile. In the second case, if you wrote Me!txtFone, you won't get a compile error. Instead, your code will blow up with a run-time error if it reaches the line of code that used Me!txtFone.
The problem with the argument against the bang operator is that this problem has nothing to do with the bang operator itself. It's behaving exactly the way it's supposed to.
When you add a control to a form, VB automatically adds a property to your form with the same name as the control you added. This property is part of the form's class, so the compiler can check for typos at compile-time if you access controls using the dot (".") operator (and you can access them using the dot operator precisely because VB created a named control property for you).
Since Me!ControlName is actually short-hand for Me.Controls("ControlName")1, it should not be suprising that you don't get any compile-time checks against mistyping the control name.
Put another way, if the bang operator is "bad" and the dot operator is "good", then you might think
Me.Controls("ControlName")
is better than
Me!ControlName
because the first version uses a dot, but in this case, the dot isn't any better at all, since you are still accessing the control name via a parameter. It's only "better" when there is an alternative way to write the code such that you will get compile-time checking. This happens to be the case with controls due to VB creating properties for each control for you, and this is why Me.ControlName is sometimes recommended over Me!ControlName.
I had originally stated that the Controls property was the default property of the Form class, but David pointed out in the comments that Controls isn't the default property of Form. The actual default property returns a collection that includes the contents of Me.Controls, which is why the bang short-hand still works.
Couple gotchas to serve as addenda to the two exceptional answers already posted:
Accessing recordset fields in forms vs. reports
The default item of Form objects in Access is a union of the form's Controls collection and the form recordset's Fields collection. If the name of a control conflicts with the name of a field, I'm not sure which object is actually returned. Since the default property of both a field and a control is their .Value, it's often a "distinction without a difference." In other words, one normally doesn't care which it is because the values of the field and control are often the same.
Beware of naming conflicts!
This situation is exacerbated by Access's Form and Report designer defaulting to naming bound controls the same as the recordset field to which they are bound. I've personally adopted the convention of renaming controls with their control type prefix (e.g., tbLastName for the text box bound to the LastName field).
Report recordset fields aren't there!
I said earlier the Form object's default item is a collection of Controls and Fields. However, the Report object's default item is only its collection of Controls. So if one wants to refer to a recordset field using the bang operator, one needs to include that field as the source for a (hidden, if desired) bound control.
Beware conflicts with explicit form/report properties
When one adds controls to a form or report, Access automatically creates properties that refer to these controls. For example, a control named tbLastName would be available from a form's code module by referring to Me.tbLastName. However, Access will not create such a property if it conflicts with an existing form or report property. For example, assume one adds a control named Pages. Referring to Me.Pages in the form's code module will return the form's Pages property, not the control named "Pages".
In this example, one could access the "Pages" control explicitly using Me.Controls("Pages") or implicitly using the bang operator, Me!Pages. Be aware, though, that using the bang operator means that Access might instead return a field named "Pages" if one exists in the form's recordset.
What about .Value?
Though not explicitly mentioned in the question, this topic came up in the above comments. The default property for Field objects and most "data-bindable"¹ Control objects is .Value. Since this is the default property, VBA will implicitly return the .Value property's value when it does not make sense to return the object itself. Thus, it's common practice to do this...
Dim EmployeeLastName As String
EmployeeLastName = Me.tbLastName
...instead of this...
EmployeeLastName = Me.tbLastName.Value
The above two statements produce identical results because EmployeeLastName is a string.
Beware the subtle .Value bug when keying dictionaries
There are some cases where this convention can cause subtle bugs. The most notable--and, if memory serves, only--one I've actually run into in practice is when using the value of a Field/Control as a Dictionary key.
Set EmployeePhoneNums = CreateObject("Scripting.Dictionary")
Me.tbLastName.Value = "Jones"
EmployeePhoneNums.Add Key:=Me.tbLastName, Item:="555-1234"
Me.tbLastName.Value = "Smith"
EmployeePhoneNums.Add Key:=Me.tbLastName, Item:="555-6789"
One would likely expect that the above code creates two entries in the EmployeePhoneNums dictionary. Instead, it throws an error on the last line because we are trying to add a duplicate key. That is, the tbLastName Control object itself is the key, not the value of the control. In this context, the control's value does not even matter.
In fact, I expect that the object's memory address (ObjPtr(Me.tbLastName)) is likely what's being used behind the scenes to index the dictionary. I did a quick test that seems to bear this out.
'Standard module:
Public testDict As New Scripting.Dictionary
Sub QuickTest()
Dim key As Variant
For Each key In testDict.Keys
Debug.Print ObjPtr(key), testDict.Item(key)
Next key
End Sub
'Form module:
Private Sub Form_Current()
testDict(Me.tbLastName) = Me.tbLastName.Value
Debug.Print ObjPtr(Me.tbLastName); "..."; Me.tbLastName
End Sub
When running the above code, exactly one dictionary item is added each time the form is closed and re-opened. Moving from record to record (and thus causing multiple calls to the Form_Current routine) does not add new dictionary items, because it is the Control object itself indexing the dictionary, and not the Control's value.
My personal recommendations/coding conventions
Over the years, I've adopted the following practices, YMMV:
Prefix Form/Report control names with control type indicators (e.g., tbTextBox, lblLabel, etc.)
Refer to Form/Report controls in code using Me. notation (e.g., Me.tbLastName)
Avoid creating table/query fields with problematic names in the first place
Use Me! notation when there are conflicts, such as with legacy applications (e.g., Me!Pages)
Include hidden report controls to gain access to report Recordset field values
Explicitly include .Value only when the situation warrants the added verbosity (e.g., Dictionary keys)
¹ What's a "data-bindable" control?
Basically, a control with a ControlSource property, such as a TextBox or ComboBox. A non-bindable control would be something like a Label or CommandButton. The default property of both a TextBox and ComboBox is .Value; Labels and CommandButtons have no default property.
I'm expecting to see a reference to VBscript Regular Expressions 5.5 by adding the path
c:\windows\system32\vbscript.dll\3 to ms-access via Tools > References. However the directory tree only shows me the full path without the ending "\3"
What does the "\3" mean (version number?), what is it the correct name for it?
How do correctly add this reference to my access project? Thanks.
Better yet, don't add a reference to it. Instead, use late binding. This means you'll use plain-vanilla object variables instead of the RegExp library's data types:
Dim objRegEx As Object
Set objRegEx = CreateObject("VBScript.Regexp")
Thus, you don't need to worry about the library version installed on the particular computer. The speed difference is pretty neglible for one call to it, but if you're going to use it regularly, create a public function like this:
Public Function RegEx() As Object
Static objRegEx As Object
If objRegEx Is Nothing Then
Set objRegEx = CreateObject("VBScript.Regexp")
End If
Set RegEx = objRegEx
End Function
Then you don't have to do anything at all -- just use RegExp the same way you'd use a variable that pointed to its top-level object. This will automatically initialize the first time you use it and will then persist until you close the application.
If you're concerned about cleaning up before closing down, you can do this:
Public Function RegEx(Optional bolClose As Boolean = False) As Object
Static objRegEx As Object
If bolClose Then
Set objRegEx = Nothing
Exit Function
End If
If objRegEx Is Nothing Then
Set objRegEx = CreateObject("VBScript.Regexp")
End If
Set RegEx = objRegEx
End Function
And in your app's shutdown routine call it thus:
Call RegEx(True)
And bob's your uncle!
bizl,
The \3 is a red herring.
To add the reference to your Access project, open any code window, select References from the Tools menu, scroll down to the entry illustrated below and check it.
Notice that the Location says \3, even though the DLL resides in the System32 directory. It has something to do with the way Microsoft versions things.
(source: windowsdevcenter.com)