Generic Way to Determine if Invoking a Property Throws an Error - vba

Say you have one slide with one chart on it, and you run this code(in a version of Office later than 2007):
Dim pptWorkbook As Object
Dim result As Object
Set pptWorkbook = ActivePresentation.slides(1).Shapes(1).Chart.ChartData.Workbook
Set result = pptWorkbook.ContentTypeProperties
You will generate an error:
Application-defined or object-defined error
I believe this is because "Smart tags are deprecated in Office 2010."(Source), Generally to avoiding this sort of issue from throwing an error and exiting your VBA you can take one of two different approaches:
//Method 1
If OfficeVersion <= 2007
Set result = pptWorkbook.ContentTypeProperties
//Method 2
On Error Resume Next // or GOTO error handler
Set result = pptWorkbook.ContentTypeProperties
Method one requires that you know the specific reason why the property would cause an error, which is easy in this case but may not be as easy with other properties. Method two requires that you use some form of error handling to deal with the error AFTER the fact, my understanding of most other Microsoft languages is that is typically discouraged(example, another example). Is this standard practice in VBA?
In VBA, is there any other way to determine whether a property of an object would throw an error if invoked, BEFORE invoking that property, and without knowing the specifics of that invoked property?

What I like to do for this situation is create a separate function that checks if the property exists and returns a Boolean. In this case it would look something like this:
Public Function CheckIfExists(targetObj As Object) As Boolean
Dim testObj As Object
On Error GoTo failedTest:
Set testObj = targetObj.ContentTypeProperties
CheckIfExists = True
Exit Function
failedTest:
CheckIfExists = False
End Function
Which would return false if that property causes an error and true if not-
Then modify your sub to be:
Public Sub FooSub()
Dim pptWorkbook As Object
Dim result As Object
Set pptWorkbook = ActivePresentation.slides(1).Shapes(1).Chart.ChartData.Workbook
If CheckIfExists(pptWorkbook) Then
Set result = pptWorkbook.ContentTypeProperties
End If
... rest of your code or appropriate error handling...
Hope this helps,
TheSilkCode

Related

using Late Binding and early binding in VBA simultaneously

I have a problem as below.
A program is written using early binding in VBA and it only works with all the references connected.
but there are some systems where the references are not connected and when the program is ran on those systems there is a compile error.
so I thought maybe I can use late binding to connect the references but it still shows compile error.
condition is that I cant convert the variables in code to late binding.
Public oPart As Part
Sub CATMain()
Dim refname()
Dim reffullPath()
Dim refGUID()
ReDim refname(5)
refname() = Array("INFITF,MECMOD", "ProductStructureTypeLib")
ReDim reffullPath(5)
reffullPath() = Array("D:\opt\ds\catia\B28_VWGROUP\win_b64\code\bin\MecModTypeLib.tlb", "D:\opt\ds\catia\B28_VWGROUP\win_b64\code\bin\PSTypeLib.tlb")
ReDim refGUID(5)
refGUID() = Array("{0D90A5C9-3B08-11D1-A26C-0000F87546FD}", "{5065F8B6-61BB-11D1-9D85-0000F8759F82}")
CheckAndAddReference refname(), reffullPath(), refGUID()
End Sub
Sub CheckAndAddReference(refname() As Variant, refLocation() As Variant, refGUID() As Variant)
Set VBAEditorx = CreateObject("MSAPC.Apc").VBE 'Application.VBE
Set vbProj = VBAEditorx.ActiveVBProject 'ActiveWorkbook.VBProject
For j = 0 To UBound(refname)
For i = 1 To vbProj.References.Count
RefCon = False
If vbProj.References.Item(i).Name = refname(j) Then
RefCon = True
Exit For
End If
Next
If RefCon = False Then
vbProj.References.AddFromFile refLocation(j)
End If
Next
End Sub
the first line "Public oPart As Part" , I cant make it as Object because there are many of these in original program.
the above line requires a reference called "INFITF,MECMOD" which is not connected in some systems.
when I try to late bind it, it is showing the error as
Compile error:
user type not defined
so I wanted to ask weather i can late bind the reference while the line "Public oPart As Part" remains same in the code without showing an error.
or I need to make all the early bound objects into "As Object".

How would you access the Shape objects WITHIN a SmartArt object without getting Type Mismatch errors?

Question: Is there any way to access Shape objects for each node inside of a SmartArt object? It seems like SmartArtNode objects have a Shapes property that could be used for this purpose, but I am getting Type mismatch errors when I try to send shapes from the SmartArtNode.Shapes to other subroutines.
Background: I am writing an iterator subroutine that takes in 2 parameters: a SmartArt object and a custom class object that has a method to perform actions on Shape objects. The subroutine is supposed to iterate over the Shape objects for each node inside the SmartArt, and call a method from the custom class object on each Shape.
Here is code for the iterator subroutine:
Public Sub IterateOverShapesInSmartArt(mySmartArt As SmartArt, manipulator As ShapeManipulator)
Dim node As SmartArtNode
Dim shpRange As ShapeRange
For Each node In mySmartArt.AllNodes
Set shpRange = node.Shapes
If shpRange.count > 0 Then
manipulator.ManipulateShape shpRange.Item(1)
End If
Next node
End Sub
For reference, the signature on the custom class (ShapeManipulator) method being called is as follows:
Public Sub ManipulateShape(myShape As Shape)
Specific Problem: When I try running this code, I get a Run-time error '13': Type mismatch triggered by the line Set shpRange = node.Shapes. Actually, I originally tried forgoing assignment of node.Shapes to a temporary variable and using the method call manipulator.ManipulateShape node.Shapes(1) instead, but then that method call produced the same Type mismatch error. I've also tried using a For loop with counter variables instead of For Each loop only to get the same error message. What is going on? When I debug, the Locals window shows the right types that match my declarations, so I am at a loss.
I tested your code and I can confirm that Set shpRange = node.Shapes causes a type mismatch error.
I noticed, though, if shpRange is declared as a generic object instead (ie Dim shpRange as Object), it gets resolved to a ShapeRange, and there's no error.
In any case, you can avoid the assignment as follows...
Public Sub IterateOverShapesInSmartArt(mySmartArt As SmartArt, manipulator As ShapeManipulator)
Dim node As SmartArtNode
For Each node In mySmartArt.AllNodes
With node
If .Shapes.Count > 0 Then
manipulator.ManipulateShape .Shapes.Item(1)
End If
End With
Next node
End Sub

What is the use of declaring an ErrObject variable if there can only ever exist one error object?

We all know there can only ever be one error object in VBA.
While helping a co-worker with error handling and why he shouldn't use On Error Resume Next I had an idea:
Store the error object somewhere to later reference back to it.
Consider this piece of test code:
Sub Test()
Dim t As ErrObject
On Error Resume Next
Err.Raise 1
Set t = Err
On Error GoTo 0
Debug.Print t.Number
On Error Resume Next
Err.Raise 1
Debug.Print t.Number
End Sub
It will print 0 to the immediate window because On Error GoTo 0 resets the error object and then prints 1 since it still holds a reference to the only error object (?).
If we create a new class and give it some properties pertaining to the ErrObject like so:
(TestClass)
Option Explicit
Public oError As ErrObject
Private Sub Class_Initialize(): End Sub
Private Sub Class_Terminate()
If Not oError Is Nothing Then Set oError = Nothing
End Sub
Public Property Get Error()
Error = oError
End Property
Public Property Set Error(ByVal ErrorObject As ErrObject)
Set oError = ErrorObject
End Property
And create our instance like this:
Sub Test2()
Dim t As TestClass
On Error Resume Next
Set t = New TestClass
Err.Raise 1
Set t.Error = Err
On Error GoTo 0
Debug.Print t.oError.Number
On Error Resume Next
Err.Raise 1
Debug.Print t.oError.Number
End Sub
We still get 0 and 1 as output respectively.
This bringst me to my question: What is the use of declaring a variable as ErrObject when we cannot create a new object itself but it simply becomes another pointer to the only error object in VBA?
None whatsoever.
Err is often treated as some kind of global ErrObject instance, but the truth is, it's a function that returns one - as revealed in the object browser:
And that function is implemented in such a way, that you always get the same object.
Objects need to expose an interface to be usable, and so the object returned by the Err function exposes that of the ErrObject class - it doesn't mean the ErrObject class exists so that it can be instantiated or encapsulated by user code: it merely provides an interface to access the properties of the current run-time error state.
When you encapsulate an ErrObject like you did, you're essentially just giving yourself another way (besides the Err function) to access the ErrObject instance - but it's still the exact same object holding the properties of the current run-time error state.
And when an object's properties change, your encapsulated copy that points to that object is going to start reporting the new values, and the old ones you meant to "remember" are overwritten.
Note that this is true for any object, not just ErrObject.
Say I have a class that does what you're doing with the ErrObject reference, but with a Collection:
Private coll As Collection
Public Property Set InternalCollection(ByVal c As Collection)
Set coll = c
End Property
Public Property Get InternalCollection() As Collection
Set InternalCollection = coll
End Property
If I make an instance of that class (let's call it Class1) and assign c to its InternalCollection, and then add items to c...
Dim c As Collection
Set c = New Collection
With New Class1
Set .InternalCollection = c
c.Add 42
.InternalCollection.Add 42
Debug.Print .InternalCollection.Count
End With
The output is 2, because c and InternalCollection (/the encapsuated coll reference) are the very same object, and that's what's happening with your encapsulated ErrObject.
The solution is to not encapsulate the ErrObject itself, but rather pull its values into backing fields for get-only properties that encapsulate the state of the ErrObject:
Private errNumber As Long
Private errDescription As String
'...
Public Sub SetErrorInfo() 'note: an ErrObject argument would be redundant!
With Err
errNumber = .Number
errDescription = .Description
'...
End With
End Sub
Public Property Get Number() As Long
Number = errNumber
End Property
Public Property Get Description() As String
Description = errDescription
End Property
'...
Now, whether that's useful is up for debate - IMO if the state is consumed at a moment where the global error state already contains the same information, there's no need to do this.
The class could pretty easily be [ab]used as a return type for a Function that returns Nothing to indicate success, and the encapsulated error state in case of failure - the problem is that the language is designed around raising errors rather than returning them; it's too easy to "fire-and-forget" such a function without verifying its return value, and since at the call site the actual runtime error state isn't going to trip an On Error statement, carrying error state as program data isn't idiomatic, it makes a "surprising" API that can easily result in code that ends up ignoring all errors.
Idiomatic error handling deals with the global runtime error state as soon as possible, and either recovers in the same scope, or lets the error state bubble up the call stack to where it can be handled. And until the error is handled, the ErrObject state is accessible through the global Err function.

Alternatives to using a Collection class

I have been looking through old code to get familiar with the system I use and found a piece of code that I feel can be used better.
What goes on here is some data gets added to the collection(around 150 string variables, some with two variables(variableName/VariableValue), most with only one(VariableName)). It will try to set a module level string variable to the item of the collection passing it the index(variableName) then if there's a value setting the VariableVAlue to the module level variable.
What I feel needs work is that if the collection is passed a variable and the variable doesn't have a value it will return a "" which would cause a runtime error hence there's a On Error GoTo Handler code to manually add a "" to the collection. I feel there's a better way to do this rather than knowing there will be a runtime issue then solving it after catching it. Would there be a way to have a return "" not throw an exception or would the use of an Array also work here since it's a "collection" as well?
Here's an example to try to help visualize:
Public Function GetCollectionVariable(ByVal varName as string) as String
If collection1 Is Nothing Then
m_collection1 = New Collection
End If
On Error GoTo Handler
GetCollectionVariable = collection1.Item(VarName)
exit function
Handler:
collection1.add("", VarName)
GetCollectionVariable = ""
End FUnction
Thanks for your time!!
If Collection1 is a dictionary, you can use TryGetValue.

How can I evaluate a string into an object in VBA?

In my previous question, How do I assign a value to a property where the property name is supplied at runtime in VBA?, I learned to use CallByName to set a property in a class at run time.
This time, however, I'm trying to figure out how to get an object at run time from a string.
For example, let's say I have a string with the following data: Worksheets("RAW DATA").Range("A1").QueryTable.
Here's what I might try to do where the data above is the input for strParam below:
Function GetObject(strParam As String) As Object
GetObject = SomeFunction(strParam)
End Function
In this case, GetObject should return a QueryTable when evaluated against Worksheets("RAW DATA").Range("A1").QueryTable. Is there anything in VBA that could take the place of SomeFunction from the example above?
Active Scripting Engine can help you. Instantiate ScriptControl ActiveX, use .AddObject() method to add reference to Excel's Application object to the script control's execution environment, set the third parameter to True to make all Application's members accessible too. Then just use .Eval() method to evaluate any property or method, which is the Application's member. The example below shows evaluation of Worksheets() property:
Sub TestQueryTable()
Dim objQueryTable As QueryTable
Dim strEvalContent As String
strEvalContent = "Worksheets(""RAW DATA"").Range(""A1"").QueryTable"
Set objQueryTable = EvalObject(strEvalContent)
objQueryTable.Refresh
MsgBox objQueryTable.Connection
End Sub
Function EvalObject(strEvalContent As String) As Object
With CreateObject("ScriptControl")
.Language = "VBScript"
.AddObject "app", Application, True
Set EvalObject = .Eval(strEvalContent)
End With
End Function
If you are on 64-bit Office, this answer may help you to get ScriptControl to work.
This time you're out of luck. There is no VBA equivalent of eval (not in Excel anyway...there is in Access VBA).
(Application.Evaluate() evaluates strings as Excel expressions, not as VBA code.)
There's the "Evaluate" method (or [ ] brackets). I don't think it will do exactly what you expect - as in run VBA code found in a string. You can look it up in the VBA help menu.