How to check if object supports method in vba? - 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

Related

Excel VBA Run-time error 438 first time through code

I'm a novice self-taught VBA programmer knowing just enough augment Excel/Access files here and there. I have a mysterious 438 error that only popped up when a coworker made a copy of my workbook (Excel 2013 .xlsm) and e-mailed it to someone.
When the file is opened, I get a run time 438 error when setting a variable in a module to a ActiveX combobox on a sheet. If I hit end and rerun the Sub, it works without issue.
Module1:
Option Private Module
Option Explicit
Public EventsDisabled As Boolean
Public ListBox1Index As Integer
Public cMyListBox As MSForms.ListBox
Public cMyComboBox As MSForms.Combobox
Public WB As String
Sub InitVariables()
Stop '//for breaking the code on Excel open.
WB = ActiveWorkbook.Name
Set cMyListBox = Workbooks(WB).Worksheets("Equipment").Listbox1
Set cMyComboBox = Workbooks(WB).Worksheets("Equipment").Combobox1 '//438 here
End Sub
Sub PopulateListBox() '//Fills list box with data from data sheet + 1 blank
Dim y As Integer
If WB = "" Then InitVariables
ListBox1Index = cMyListBox.ListBoxIndex
With Workbooks(WB).Worksheets("Equipment-Data")
y = 3
Do While .Cells(y, 1).Value <> ""
y = y + 1
Loop
End With
Call DisableEvents
cMyListBox.ListFillRange = "'Equipment-Data'!A3:A" & y
cMyListBox.ListIndex = ListBox1Index
cMyListBox.Height = 549.75
Call EnableEvents
End Sub
...
PopulateListBox is called in the Worksheet_activate sub of the "Equipment" sheet.
All my code was in the "Equipment" sheet until I read that was bad form and moved it to Module1. That broke all my listbox and combobox code but based on the answer in this post I created the InitVariables Sub and got it working.
I initially called InitVariables once from Workbook_open but added the If WB="" check after WB lost its value once clicking around different workbooks that were open at the same time. I'm sure this stems from improper use of Private/Public/Global variables (I've tried understanding this with limited success) but I don't think this is related to the 438 error.
On startup (opening Excel file from Windows Explorer with no instances of Excel running), if I add a watch to cMyComboBox after the code breaks at "Stop" and then step through (F8), it sets cMyComboBox properly without error. Context of the watch does not seem to affect whether or not it prevents the error. If I just start stepping or comment out the Stop line then I get the 438 when it goes to set cMyComboBox.
If I add "On Error Resume Next" to the InitVariables then I don't error and the project "works" because InitVariables ends up getting called again before the cMyComboBox variable is needed and the sub always seems to work fine the second time. I'd rather avoid yet-another-hack in my code if I can.
Matt
Instead of On Error Resume Next, implement an actual handler - here this would be a "retry loop"; we prevent an infinite loop by capping the number of attempts:
Sub InitVariables()
Dim attempts As Long
On Error GoTo ErrHandler
DoEvents ' give Excel a shot at finishing whatever it's doing
Set cMyListBox = ActiveWorkbook.Worksheets("Equipment").Listbox1
Set cMyComboBox = ActiveWorkbook.Worksheets("Equipment").Combobox1
On Error GoTo 0
Exit Sub
ErrHandler:
If Err.Number = 438 And attempts < 10 Then
DoEvents
attempts = attempts + 1
Resume 'try the assignment again
Else
Err.Raise Err.Number 'otherwise rethrow the error
End If
End Sub
Resume resumes execution on the exact same instruction that caused the error.
Notice the DoEvents calls; this makes Excel resume doing whatever it was doing, e.g. loading ActiveX controls; it's possible the DoEvents alone fixes the problem and that the whole retry loop becomes moot, too... but better safe than sorry.
That said, I'd seriously consider another design that doesn't rely so heavily on what appears to be global variables and state.

Why would VBA TypeOf operator fail

I have been fighting with an Excel 2007 problem for several days now. Below is a listing of all facts I can think of that might be relevant:
IDetailSheet is a class declared in the VBA project with several methods, and which throws an error in its Class Initializer so that it cannot be instantiated (making it abstract).
Option Explicit is set in all modules.
Ten worksheets in the VBA project implement IDetailSheet and compile cleanly (as does the entire project).
CDetailSheets is a class declared in the VBA project that wraps a Collection object and exposes the Collection object as a Collection of IDetailSheet. It also exposes some additional methods to perform certain methods of IDetailSheet on all collection menmbers.
In its Class initializer (called from the Workbook_ Open event handler and assigned to a global variable), CDetailSheet executes the following code to populate the private collection DetailSheets:
Dim sht as EXCEL.WorkSheet
For Each sht in ActiveWorkbook.Worksheets
If TypeOf sht is IDetailSheet Then
Dim DetailSheet as IDetailSheet
Set DetailSheet = sht
DetailSheets.Add DetailSheet, DetailSheet.Name
End If
Next sht
In certain Ribbon call-backs the following code is run:
If TypeOf ActiveWorkbook.ActiveSheet is IDetailSheet Then
Dim DetailSheet as IDetailSheet
Set DetailSheet = ActiveWorkbook.ActiveSheet
DetailSheet.Refresh *[correction]*
End If
All ActiveX controls have been removed from the Workbook, after having been identified with other stability issues (There were a few dozen originally). A Fluent Interface Ribbon has been created to replace the functionality originally associated with the ActiveX controls.
There is a Hyperion add-in from the corporate template, but it is not used in this workbook.
When all is said and done, the following symptom occurs when the workbook is run:
Any number of instances of IDetailSheet are recognized in the CDetailSheets Initializer by TypeOf Is, from 1 (most common) to occasionally 2 or 3. Never zero, never more than 3, and most certainly never the full 10 available. (Not always the same one, though being near the front of the set seems to increase likelihood of being recognized.)
Whichever instances of IDetailSheet implementation are discovered in the CDetailSheets initializer (and as near as I can determine, only such instances) are also recognized by TypeOf ... Is in the Ribbon call-back.
Can anyone explain why most of the TypeOf ... Is operations are failing? Or how to fix the issue?
I have resorted to manually creating v-tables (i.e. big ugly Select Case ... End Select statements) to get the functionality working, but I actually find it rather embarrassing to have my name beside such code. Besides which, I can see that being a future maintenance nightmare.
Thinking that it might be a stale p-code issues, I went to the extent of deleting the Project.Bin file from the expanded XLSM zip, and then manually importing all the VBA code back in. No change. I also tried adding the project name to all the usages of IDetailSheet to make them miFab.IDetailSheet, but again to no avail. (miFab is the project name.)
There are a few ways you could cheat using CallByName. You're going to have to work around this bug one way or another.
A quick dirty example
Every sheet that starts with an implementing line should have a public GetType function.
I attached the "TestSheet" sub to a button on my ribbon. It puts the returned type name in cell A1 to demonstrate the function.
Module1
'--- Start Module1 ---
Option Explicit
Public Sub TestSheet()
Dim obj As Object
Set obj = ActiveSheet
ActiveSheet.[A1] = GetType(obj)
End Sub
Public Function GetType(obj As Object) As String
Dim returnValue As String
returnValue = TypeName(obj)
On Error Resume Next
returnValue = CallByName(obj, "GetType", VbMethod)
Err.Clear
On Error GoTo 0
GetType = returnValue
End Function
'--- End Module1 ---
Sheet1
'--- Start Sheet1 ---
Implements Class1
Option Explicit
Public Function Class1_TestFunction()
End Function
Public Function GetType() As String
GetType = "Class1"
End Function
'--- End Sheet1 ---
I found this question after posting my own similar issue as TypeOf fails to work with Excel workbook's ActiveSheet that implements interface
I don't have a definitive explanation, but I think I do have a workaround.
I suspect it is because [the code] is implementing an interface on Sheet1 or Chart, and is extending Sheet1/Chart1, but Sheet1 is already extending Worksheet (and Chart1 is already extending Chart).
In my testing, I am able to force VBA to return the real value of TypeOf by firstly accessing a property of the sheet. That means, doing something ugly like:
'Explicitly access ThisWorkbook.ActiveSheet.Name before using TypeOf
If TypeOf ThisWorkbook.Sheets(ThisWorkbook.ActiveSheet.Name) Is PublicInterface Then
If you're not trusting TypeOf, plough on and ignore errors:
Dim sht as EXCEL.WorkSheet
For Each sht in ActiveWorkbook.Worksheets
'If TypeOf sht is IDetailSheet Then
Dim DetailSheet As IDetailSheet
On Error Resume Next
Set DetailSheet = sht
On Error GoTo 0
If Not DetailSheet Is Nothing Then
DetailSheets.Add DetailSheet, DetailSheet.Name
End If
Next sht
If this doesn't work, the worksheets really aren't IDetailSheet at that time at least.

VBA: What happens to Range objects if user deletes cells?

Suppose I have some module in vba with some variable r of type Range. Suppose that, at some point, I store a Range object there (e.g. the active cell). Now my question: What happens to the value of r if the user deletes the cell (the cell, not only its value)?
I tried to figure this out in VBA, but without success. The result is strange. r is not Nothing, the value of r is reported to be of type Range, but if I try to look at its properties in the debugger window, each property's value is reported as "object required".
How can I, programmatically, determine whether variable r is in this state or not?
Can I do this without generating an error and catching it?
Nice question! I've never thought about this before, but this function will, I think, identify a range that was initialzed - is not Nothing - but is now in the "Object Required" state because its cells were deleted:
Function RangeWasDeclaredAndEntirelyDeleted(r As Range) As Boolean
Dim TestAddress As String
If r Is Nothing Then
Exit Function
End If
On Error Resume Next
TestAddress = r.Address
If Err.Number = 424 Then 'object required
RangeWasDeclaredAndEntirelyDeleted = True
End If
End Function
You can test is like this:
Sub test()
Dim r As Range
Debug.Print RangeWasDeclaredAndEntirelyDeleted(r)
Set r = ActiveSheet.Range("A1")
Debug.Print RangeWasDeclaredAndEntirelyDeleted(r)
r.EntireRow.Delete
Debug.Print RangeWasDeclaredAndEntirelyDeleted(r)
End Sub
I believe that when you use the Set keyword in VBA, it creates a pointer in the background to the worksheet's Range object in the worksheet you specified (each cell being an object in the collection of Cells of the Worksheet for a given Range). When the range is deleted while you are still referencing it in memory, the memory for the object that the Range variable was pointing to has been deallocated.
However, your Range variable most-likely still contains the pointer to the recently removed Range object, which is why it isn't nothing, but whatever it's pointing to doesn't exist anymore, which causes problems when you try to use the variable again.
Check out this code to see what I mean:
Public Sub test2()
Dim r As Excel.Range
Debug.Print ObjPtr(r) ' 0
Set r = ActiveSheet.Range("A1")
Debug.Print ObjPtr(r) ' some address
r.Value = "Hello"
r.Delete
Debug.Print ObjPtr(r) ' same address as before
End Sub
Check out this article for more info about ObjPtr():
http://support.microsoft.com/kb/199824
So while you have a valid address to an object, unfortunately the object doesn't exist anymore since it has been deleted. And it appears that "Is Nothing" just checks for an address in the pointer (which I think VBA believes that the variable is "Set").
As to how to get around this problem, unfortunately I don't see a clean way of doing it at the moment (if anyone does find an elegant way to handle this, please post it!). You can use On Error Resume Next like so:
Public Sub test3()
Dim r As Excel.Range
Debug.Print ObjPtr(r) ' 0
Set r = ActiveSheet.Range("A1")
Debug.Print ObjPtr(r) ' some address
r.Value = "Hello"
r.Delete
Debug.Print ObjPtr(r) ' same address as before
On Error Resume Next
Debug.Print r.Value
If (Err.Number <> 0) Then
Debug.Print "We have a problem here..."; Err.Number; Err.Description
End If
On Error GoTo 0
End Sub
How can I, programmatically, determine whether variable r is in this
state or not?
Can I do this without generating an error and catching it?
No.
To the best of my knowledge, you can't test for this condition reliably: not without raising and catching an error.
Your question has been noticed and discussed elsewhere: Two of the big names in Excel/VBA blogging (Dick Kusleika and Rob Bovey) have looked into it, and you may find something informative in there. But the answer's No.
All in all, a good question with rather worrying answer.
To test if a range object is currently invalid, I use this function:
Public Function InvalidRangeReference(r As Range) As Boolean
On Error Resume Next
If r.Count = 0 Then
InvalidRangeReference = Err
End If
End Function

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

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.

Methods in EXCEL Addin - XLL

How do I know which methods are available in my XLL module, in case i need to use / call any of them in my VBA code.
I can do this by calling the:
Application.Run()
method, in which I have to pass my macro-name as the parameter.
My question is about this macro-name: how do I know which macros are present in my XLL addin.
Any help is appreciated.
Cheers!!!!!!!!!!!
Tushar
You can use the Application.RegisteredFunctions method to give you a list of the functions in the XLLs that Excel has registered.
For example, the following code will list the XLL, the function name and the parameter types for the XLLs that are currently registered:
Public Sub ListRegisteredXLLFunctions()
Dim RegisteredFunctions As Variant
Dim i As Integer
RegisteredFunctions = Application.RegisteredFunctions
If IsNull(RegisteredFunctions) Then
Exit Sub
Else
Dim rng As Range
Set rng = Range("A1")
Set rng = rng.Resize(UBound(RegisteredFunctions, 1), UBound(RegisteredFunctions, 2))
rng.Value = RegisteredFunctions
End If
End Sub
Are you asking this from a code P.O.V? If you just want to check it out manually you can see that in the project explorer. Otherwise, I'd suggest just attempting to run the macro, but use an error handler in case the macro doesn't exist.
On Error GoTo badMacroCall
application.run(myMacro)
badMacroCall:
msgbox("That macro could not be run!")