Microsoft VBA idiom (Visio) For Testing Non-Existence of a property? - vba

I need to ensure a Macro which works on Visio 2003 doesn't cause problems on lower versions of Visio: specifically because I'm writing to a property which doesn't exist on lower versions of Visio. Currently I'm doing this:
...
On Error GoTo NoComplexScriptFont:
Set cellObject = shapeObject.Cells("Char.ComplexScriptFont")
On Error GoTo ErrHandler
...
NoComplexScriptFont:
Rem MSGBOX only for debug
MsgBox "No Such Property"
GoTo endsub
ErrHandler:
Rem put in general error handling here
GoTo endsub
endsub:
End Sub
...
Which works, but its a little messy I think. I have toyed with the idea of using 'Application.version' (Which returns '11' for Visio 2003), but I would like to avoid assumptions about what properties are available in any particular release and just test for the property itself.
What's the nice proper idiom for doing this in VBA ?
Thanks
--- Got a few answers below, my preferred solution was this one:
If shapeObject.CellExists("Char.ComplexScriptFont", 0) Then
msgbox "Property exists"
else
msgbox "Property does not exist"
end if

I would use a wrapper function for accessing the property so that you don't mess up your normal error handling, like this:
...
Set cellObject = GetCellObject(shapeObject)
If Not cellObject Is Nothing Then
' Do something with cellObject
End If
...
Private Function GetCellObject(ByVal shapeObject As Object) As Object
On Error Resume Next
Set GetCellObject = shapeObject.Cells("Char.ComplexScriptFont")
End Function
(Note: I'm only using Object above because I don't know what type cellObject etc. is)
I often use the same technique even for properties that I know do exist, but which will raise an error under certain circumstances. For example (Excel), if I'm going to access a worksheet by name (which will raise an error if no such worksheet exists), then I'll have a wrapper function that calls Worksheets(name) and either returns a Worksheet object or Nothing:
Private Function GetWorksheet(ByVal strName as String) As Worksheet
On Error Resume Next
Set GetWorksheet = Worksheets(strName)
End Function
This makes for much cleaner calling code, since you can simply test the return value rather than worrying about error handling.

You can use the CellExists property of the shape object to see if a particular cell exists. You have to pass in the localeSpecificCellName, which you already seem to be using, and then you pass in an integer fExistsLocally, which specifies the scope of the search for the cell; if you specify 0 then the CellExists will return true if the cell is inherited or not...if it's 1 then CellExists will return false if the cell is inherited.

Related

How to check if object supports method in vba?

Is it possible to check whether an object supports a certain method without an error handler in VBA?
I've found numerous duplicates asking the question for for example JavaScript and Symphony2, but not yet in VBA.
I would like to use a .sendkeys "{ENTER}" command to an ie.document class item and learning how to check whether the object supports a method allows me to write cleaner code in the long run.
example code:
Set elements(17) = ie.document.getElementsByClassName("ng-binding ng-scope")
for each item in elements(17)
item.sendkeys "{ENTER}"
next item
Short of looking at the documentation for the API you're using, you can't.
At least not on late-bound code. By definition, late-bound code is resolved at run-time, which means you have no compile-time way of validating whether a member will be available on that object's interface - that's why this code compiles:
Option Explicit
Public Sub Test(ByVal o As Object)
Debug.Print o.FooBarBazz
End Sub
Or this somewhat more realistic one:
Debug.Print ThisWorkbook.Worksheets("Test").Naame 'Worksheets.Item returns an Object
The only way to know before run-time whether a member is supported, is to use early-bound calls:
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("Test")
Debug.Print ws.Naame ' typo won't compile!
Of course it's not that simple, because even early-bound interfaces can have a COM flag saying they're "extensible". Excel.Application is one such interface:
Debug.Print Excel.Application.FooBarBazz ' compiles!
But I'm drifting.
You say "without error handling" ...does this count?
On Error Resume Next
item.sendkeys "{ENTER}"
'If Err.Number = 438 Then Debug.Print "Not supported!"
On Error GoTo 0

Determining if a cell is linked to a QueryTable via VBA in Excel

I'm linking an Excel worksheet to a data source using QueryTables which works well. I'd like to introduce logic to check a given cell and determine if that cell is the top left corner of an existing QueryTable. This seemed trivial in concept, but has proven elusive in practice. If I try to check any properties of the cell that relate to QueryTable I get an error. i.e. rng.querytable throws error 1004 if rng does not link to a QueryTable. So I messed around with testing if rng.ListObject is nothing based on some discussions I found online. It turns out that rng.ListObject is something if the cell is a table even if that table is not a QueryTable. So no dice there.
So how do I test if a target cell contains a QueryTable?
Here's a stub of my function I got from Mr. Excel:
Public Function IsRangeInQueryTable(rngCheck As Range) As Boolean
Dim QT As QueryTable
For Each QT In rngCheck.Parent.QueryTables
If Not Intersect(rngCheck, QT.ResultRange) Is Nothing Then
IsRangeInQueryTable = True
Exit Function
End If
Next QT
'If it got to here then the specified range does not lie
'in the result range of any query
IsRangeInQueryTable = False
End Function
The function above works in many cases, but if I have QueryTables that have become detached from their destination (which seems to happen in practice) the code throws error 1004 because the QueryTable has no destination. Here's what the watch shows when the QueryTable becomes decoupled from a destination:
FWIW, I also tried approaching this the reverse way and iterating through every QueryTable in a worksheet. It turns out if there's a QueryTable which has had its destination deleted, asking that QueryTable for its QueryTable.Destination throws an app error. So I could not figure out a reliable way to do that approach either.
This is wordier than it needs to be, but because the "OnError" statement redirects all errors, I want to eliminate the possibility of accidentally handling the wrong error, for example if rng was Nothing.
Public Function cell_has_query(rng As Range) As Boolean
If rng Is Nothing Then
cell_has_query = False
Exit Function
End If
If rng.ListObject Is Nothing Then
cell_has_query = False
Exit Function
End If
On Error GoTo ErrHandler
If Not rng.ListObject.QueryTable Is Nothing Then
cell_has_query = True
End If
Exit Function
ErrHandler:
If Err.Number = 1004 Then 'Application-Defined or Object-Defined Error
cell_has_query = False
Else
On Error GoTo 0
Resume
End If
End Function
#JDLong VBA error handling is odd. The default setting for errors is On Error GoTo 0, which means that (depending upon your VBA IDE settings; Tools-->Options-->General) will pop up a message box for unhandled errors. If you want to explicitly catch and handle errors, you create a label (e.g. "ErrHandler") and then make sure that section of code is unreachable normally by ending the function with Exit Function. In the block of code after the label, you can inspect the Err object properties and choose to Resume to retry the line of code that caused the error, Resume Next to run the line following the one that errored, or simply handle the error and let the function exit normally. You can also reraise the error by setting the mode back to On Error GoTo 0 and then Resumeing the line.
for some reason that is not clear to me, my querytables are not in listobjects. I thought all querytables are in listobjects, but I've run enough tests to demonstrate to myself that mine are not. So I slightly edited #blackhawk's function to this:
Public Function cell_has_query(rng As Range) As Boolean
If rng Is Nothing Then
cell_has_query = False
Exit Function
End If
On Error GoTo ErrHandler
If Not rng.QueryTable Is Nothing Then
cell_has_query = True
End If
Exit Function
ErrHandler:
If Err.Number = 1004 Then 'Application-Defined or Object-Defined Error - this throws if there is a querytable with no destination
cell_has_query = False
Else
On Error GoTo 0
Resume
End If
End Function

How to check the availability of a worksheet

I have to run a set of code related to worksheet "wins", but only if that worksheet exist.
Please share a code to check the availability of sheet "wins". If worksheet "wins" exist, then only I want to run that set of code, else I want to skip executing that set of code and move to next line of code.
You could use On Error Resume Next to skip the errror which occurs if you try access a not existing worksheet and assigning it to a object variable. So if the worksheet does not exist, no error occurs but the variable is Nothing. If the worksheet exists, then the variable is not Nothing.
Example:
Sub test()
Dim wsWins As Worksheet
On Error Resume Next
Set wsWins = ActiveWorkbook.Worksheets("wins")
On Error GoTo 0
If Not wsWins Is Nothing Then
MsgBox "Worksheet wins exists."
Else
MsgBox "Worksheet wins does not exist."
End If
End Sub
Axel's answer will work nicely. Some people prefer not to use error throwing to test if something exists. If you're one of them then I use the following quite a lot in a Utility module. It'll work for Worksheets, Charts, etc. (basically anything that's a collection with a 'Name' property):
Public Function ExcelObjectExists(testName As String, excelCollection As Object) As Boolean
Dim item As Object
On Error GoTo InvalidObject
For Each item In excelCollection
If item.Name = testName Then
ExcelObjectExists = True
Exit Function
End If
Next
ExcelObjectExists = False
Exit Function
InvalidObject:
MsgBox "Developer error: invalid collection object passed in ExcelObjectExists."
ExcelObjectExists = False
End Function
You can call it like this:
If ExcelObjectExists("wins", ThisWorkbook.Worksheets) Then

VBA: Two methods of seeing if a file is open

I've got two methods which I feel should tell if a file is open or not.
Method 1 (Error: Subscript out of range):
If Not Workbooks(filename) Is Nothing Then
Workbooks.Open (filename)
End If
Method 2:
If Not IsWorkbookOpen(filename) Then
Workbooks.Open (filename)
End If
Where IsWorkbookOpen() is:
Private Function IsWorkbookOpen(wbname) As Boolean
Dim wBook As Workbook
Set wBook = Nothing
On Error Resume Next
Set wBook = Workbooks(wbname)
If wBook Is Nothing Then
IsWorkbookOpen = False
Else: IsWorkbookOpen = True
End If
End Function
Aside from On Error Resume Next, Method 1 appears to be nearly the same as Method 2.
Could anyone please explain why Method 1 gives the error it does?
Thank you.
They both give a subscript out of range error. But in Method 2 you suppress that error with On Error Resume Next.
Sub SeeError()
On Error Resume Next
Debug.Print Workbooks("DoesNotExist").Name
Debug.Print Err.Description
End Sub
This prints "Subscript Out of Range" in the Immediate Window. The On Error statement doesn't stop the error from occurring, it just handles it. Method 1 doesn't handle errors so the default error handling (stop execution and report the error) is in effect.
VBA tries to evaluate all the parts before it evaluates the conditional statement. So if I have a variable myvar = "xyz" and try to run the following lines...
If IsNumeric(myvar) And Round(myvar, 1) = 3 Then
'you will get an error before the IF is evaluated
End If
it will not work. VBA will evaluate IsNumeric(myvar) fine, then try to evaluate Round(myvar, 1) = 3 and get an error before it checks the entire conditional. So VBA will let you know about the error before it performs the AND operator. If VBA had short circuit evaluation, it would work fine since the first part would evaluate to false.
But the following will work
If IsNumeric(myvar) Then
If Round(myvar, 1) = 3 Then
'second IF statement is not touched since first IF statement evaluates to false
End If
End If
This works because IsNumeric(myvar) evaluates to false and therefore skips the nested statement.
So the error it throws on the Workbooks(filename) will just give the error unless you tell it to resume next. So the method I use is
On Error Resume Next
Set wb = Workbooks(file)
If wb Is Nothing Then
Set wb = Application.Workbooks.Open(dir & "\" & file, ReadOnly:=True)
End If
On Error GoTo 0
Edited to give more detail and correctly capture that the second example will not be evaluated as well as provide a useful solution for the question at hand.
Workbooks(filename) tries to get the element with the identifier (or 'index') filename from the collection Workbooks. If the collection does not contain such an element, you'll get the "Subscript out of range" error. (So in a nutshell: This code will fail whenever the file is not open. You probably don't want this.)
However, the knowledge that such an access will fail if the file is not open, i.e. an error is raised, is being made use of in the second method. The code tries to get the element with the identifier filename from the Workbooks collection and to assign it to the variable wBook. If it fails, the value of the variable wBook will stay Nothing. If it succeeds, the variable wBook will contain a reference to the respective Workbook object.

Excel VBA still breaks after On Error handler

I have a very simple bit of code that loops through a column of data and adds unique values to a Collection.
It's VBA, so of course Collection lacks an Exists function (who'd ever want that?), and I'd rather avoid iterating over the entire collection for every cell in the column, I decided to go for the error-handling approach - attempt to retrieve the item from the collection, catch the error that occurs if it doesn't exist and add it:
'Trucated the code slightly, I know I should be checking the actual error code, but omitted that for brevity
Dim r As Range
Set r = MySheet.Range("B2") 'First cell in column
Dim uniqueValues As New Collection
Do While r.Value <> ""
On Error GoTo ItemExists
'If r.Value doesn't exist in the collection, throws an error
uniqueValues.Add(Item:=r.Value, Key:=r.Value)
ItemExists:
r.Offset(1)
Loop
The problem? Excel seems to be completely ignoring the On Error line, breaking the code and throws up the Continue/End/Debug dialog regardless.
I've checked the options in VBA, it's correctly set to Break on Unhandled Errors.
Any idea why this is happening?
You could use On Error Resume Next, but you're better off encapsulating the error handling in its own Sub or Function. E.g. something like:
Private Sub AddIfNotPresent(Coll As Collection, Value As Variant, Key As Variant)
On Error Resume Next
Coll.Add Item:=Value, Key:=Key
End Sub
which you could use as follows:
Do While r.Value <> ""
AddIfNotPresent uniqueValues, r.Value, r.Value
r = r.Offset(1)
Loop
The reason for your problem is described in the VBA documentation for On Error:
An "enabled" error handler is one that is turned on by an On Error statement; an "active" error handler is an enabled handler that is in the process of handling an error. If an error occurs while an error handler is active (between the occurrence of the error and a Resume, Exit Sub, Exit Function, or Exit Property statement), the current procedure's error handler can't handle the error
You have not called Resume or exited the procedure after the first error, so the error handler can't handle subsequent errors.
UPDATE
From comments:
I hate the idea of promoting the use of On Error Resume Next ...
I can sympathise with this POV, but there are some things in VBA (e.g. checking if a key is present in a Collection) that you can only do by handling an error. If you do this in a dedicated helper method (Sub/Function), it's a reasonable approach. Of course, you can use On Error Goto instead, e.g. something like the following (a variation on the above which tests to see if a collection contains a given key):
Public Function ContainsKey(Coll As Collection, Key As Variant) As Boolean
On Error GoTo ErrHandler
Dim v As Variant
v = Coll(Key)
ContainsKey = True
Exit Function
ErrHandler:
ContainsKey = False
Exit Function
End Function
use an ArrayList instead, which has an .Contains method amongst other handy things like a .Sort method
With CreateObject("System.Collections.ArrayList")
.Add "Item 1"
.Add "Item 2"
If .Contains "Item 1" Then Msgbox "Found Item 1"
If .Contains "Item 3" Then Msgbox "Found Item 3"
End With
some more examples here