Using the Find VBA function with Defined String - vba

Hey maybe I'm not seeing something obvious here but how can you use the Find VBA function with a predefined variable. I'm using a concatenation of a string assigned from a user form and just "total " in front of it, yet I can't return the row.
Below is my code
Dim HBWS As Worksheet
Dim TickerString As String
TickerString = "Total " & TTB
Set HBWS = Sheets("Hoenheimm Worksheet")
BorrowColumn = HBWS.Cells.Find(What:="Borrow").Column 'Works just fine
TickerRow = HBWS.Cells.Find(What:=TickerString).Row 'Throws an error
Note that TTB is set to a ticker ex. AAPL, and I can check in my local windows that Tickerstring is in fact = to "Total AAPL"
I would expect the .Row column to give me the row on my worksheet as to where this string is located.
EDIT: the error being thrown is as follows...
"Run-Time error '91':
Object Variable or With block variable not set"
Any throughts,
Thanks

You're invoking Range.Find. That method returns a Range object reference - and when it does not find what it's told to look for, it returns Nothing, i.e. a null reference.
TickerRow = HBWS.Cells.Find(What:=TickerString).Row 'Throws an error
What this code is doing (and the working instruction just above it), is assuming that Find returns a valid object reference.
Apparently HBWS.Cells does not contain "Total " & TTB (whatever TTB is), so your code is effectively trying to invoke Range.Row against a Range reference that's Nothing... which is illegal, and raises run-time error 91 as you're experiencing.
You shouldn't assume that Find will return a valid reference, ever. Split it up, and validate the returned object reference with an If ... Is Nothing check:
Set tickerResult = HBWS.Cells.Find(What:=TickerString)
If Not tickerResult Is Nothing Then
tickerRow = tickerResult.Row
Else
'tickerString was not found.
'watch for leading/trailing/extra spaces if the string does *look* legit.
End If
When calling Range.Find, you should always provide a value for the optional parameters, because its implementation "remembers" values from previous invocations, and this is easily bug-prone.

Related

Generic Way to Determine if Invoking a Property Throws an Error

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

Conditional guard IF Not (x = Empty) Then

I have the following
Public Sub BreakAllLinks(ByRef aWkBook As Excel.Workbook)
Dim Link As Variant
Dim myLinks As Variant
myLinks = aWkBook.LinkSources(Type:=Excel.xlLinkTypeExcelLinks)
If Not (myLinks = Empty) Then
For Each Link In myLinks
aWkBook.BreakLink Name:=Link, Type:=Excel.xlLinkTypeExcelLinks
Next Link
End If
End Sub 'BreakAllLinks
If myLinks is empty then it works well and avoids the For Each loop but if myLinks contains some links then I get the following error
Runtime error '13'
What is wrong with If Not (myLinks = Empty) Then?
LinkSources returns either Empty or an array.
You cannot compare an array with Empty using the equality operator.
The documentation shows you the right way of checking the result - using the IsEmpty function.
The function succeeds regardless of the value type stored in your Variant.
If e.g. you had a Nothing in there, you would get error 91.
Or, if you had an object reference in there, your comparison would try to fetch the default property of the stored object and compare that to Empty.
Which is why you should never check for = Empty really, and only use IsEmpty.

Reference to Window Object in VBA

I have a problem referencing a windows object in VBA. it throws the following error: "Error 5 (Invalid procedure call or argument). I cannot find the cause, because I see no programming error.
Public Sub TestWindowhandle()
Dim lResult As Long
Dim objShell, wins, winn
Dim IE_Count As Long, i As Long, This_PID As Long
On Error GoTo TestWindowhandle_Error
Set objShell = CreateObject("Shell.Application")
Set wins = objShell.Windows
IE_Count = wins.Count
For i = 0 To (IE_Count - 1)
Set winn = wins.Item(i)
Next i
On Error GoTo 0
Exit Sub
TestWindowhandle_Error:
MsgBox "Error " & Err.Number & " (" & Err.Description & ") in line " & Erl & " in procedure TestWindowhandle of Module Module1"
Stop
End Sub
Something odd with that interface, it seems to only work with a copy of the control variable so:
Set winn = wins.Item(i + 0)
or
Set winn = wins.Item((i))
I believe here is what is happening.
The Item method accepts a Variant parameter.
When calling an external method that accepts a Variant parameter, VB likes to create and pass Variants that provide the value by reference - that is, with the VT_BYREF flag set.
However VB does not set this flag when sending out intermediate results (temporaries, not stored in a variable), which makes sense because even if the called method updates the value, no one will be able to see it.
So when you call .Item(i), VB sends out a Variant of type VT_I4 | VT_BYREF, and when you call .Item(i + 0), a Variant of type VT_I4 is dispatched, without the VT_BYREF.
In most situations the difference is not significant because VARIANT-aware methods should be able to cope with either. However this particular method does different things depending on exactly which VT_ it receives, and because of this it is explicitly willing to reject any VT_s other than the three accepted ones. So in order to call it you need to trick VB into removing the byref flag.
Interestingly, when you declare the variable as Variant, VB still dispatches a VT_VARIANT | VT_BYREF, but the method seems to support this situation and correctly resolves to the pointed-to inner Variant that has the non-reference type of VT_I4.
Note this has nothing to do with VB's ByVal/ByRef - this is about the internal structure of the VARIANT data type.

VBA function result type mismatch

I am trying to run as a macro a custom Excel function XpathOnUrl from the add-in called SeoTools by Niels Bosma. The function runs fine and it seems that I am able to store its result in a variable. This variable can then be correctly output to an Excel cell, but when I try to look for a string in it in the next part of the macro, I get the error Run-time error '13': Type mismatch. From what I understand from here, the function returns an array, but when I try to access it as the first item of the array, I get the same error. I tried to convert the variable into a string with CStr, but no luck there either. What am I missing?
Here's the problematic part of the code:
WebSite = Sheet1.Range("A1")
contactPage = Application.Run("XPathOnUrl", WebSite, "//a[contains(translate(#href, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'),""contact"")]", "href")
MsgBox TypeName(contactPage) 'trying to find out the data type results in 'Error'
If Left(contactPage(0), 4) = "http" Then
Sheet1.Range("B1").Value = contactPage
ElseIf InStr(contactPage, "/") = 1 Then
Sheet1.Range("B1").Value = WebSite & contactPage
End If
Just to make it clear: the problem starts only with conditional statements. If I assign the value of the variable directly to a cell like this Sheet1.Range("B1").Value = contactPage, it outputs the correct result.
Here's a easy workaround:
Make XpathURL spit its return to a range. Then, use Range.value to assign the return to a contactpage and clear the range using .Clearcontents property. I think Application.Run is not letting XpathURL return get to contactpage.
Edit: Added the comment below:
Sheet1.Range("B1").Value = Application.Run("XPathOnUrl", WebSite, "//a[contains(translate(#href, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'),""contact"")]", "href")
contactPage = Sheet1.Range("B1").Value
Did you try the Formula and FormulaR1C1 functions? Just my assumptions.
Try and let us know if it helped.
Sheet1.Range("B1").Formula = WebSite & contactPage
or
Sheet1.Range("B1").FormulaR1C1 = WebSite & contactPage
What does the MsgBox TypeName(contactPage) display?

Error in using QueryDef to define parameters in a query

I'm working in Access and trying to use a query with parameters in VBA. I have several queries that I need to use, so I added a routine to generalize the process:
Public Function Execute_query(query) As Recordset
Dim qdf As QueryDef
Set qdf = CurrentDb.QueryDefs(query)
For Each prm In qdf.Parameters
prm.Value = Eval(prm.Name)
Next prm
If (qdf.Type = 80) Then
qdf.Execute
Else: Set Execute_query = qdf.OpenRecordset
End If
End Function
I'm still testing this so there may be other issues, but my immediate question is why the Eval(prm.name) line isn't working. The paramater is [R_Yr] which I've declared as a public variable and have assigned a value - which I can verify in the watch window. But I get an error code 2482 - Access cannot find the name 'R_yr"
This same code seems to work when the parameter value is coming from a form instead of a variable - which is why I had to set it up in the first place - I couldn't access a form control in a query run from VBA.
Forgive me, but I'm having a bit of trouble seeing the real benefit from this extra level of indirection. You have to create variables that correspond to the parameters for the particular query you are planning to invoke, and then you call a generic function to invoke it. Why not just create a QueryDef, pass it the parameters, and invoke it in place? It seems like essentially the same amount of work, and it makes the code easier to follow because "everything is right there".
Eval() takes the string you give it and uses the "expression service" to process it. The problem you're facing is the expression service doesn't know anything about VBA variables. If you're really determined, you may be able to figure out a workaround which builds the variable's value rather than the variable's name into the string you give Eval() to ... um ... evaluate.
But for what you're doing, I suggest you ditch Eval(). Instead give the function a data structure such as a Scripting.Dictionary or VBA Collection which contains the parameter values with your former variable names as keys.
Here is a VBA Collection example ...
Dim MyCol As Collection
Set MyCol = New Collection
MyCol.Add CLng(10), "R_Yr"
MyCol.Add "foo", "MyString"
Debug.Print MyCol("R_Yr"), TypeName(MyCol("R_Yr"))
Debug.Print MyCol("MyString"), TypeName(MyCol("MyString"))
That code gives me this output in the Immediate window ...
10 Long
foo String
So consider building a similar collection in the calling code and passing that collection to a modified Execute_query function.
Public Function Execute_query(ByVal pQdf As String, _
ByRef pCol As Collection) As Recordset
Dim qdf As QueryDef
Set qdf = CurrentDb.QueryDefs(pQdf)
For Each prm In qdf.Parameters
prm.Value = pCol(prm.Name)
Next prm
If (qdf.Type = 80) Then
qdf.Execute
Else
Set Execute_query = qdf.OpenRecordset
End If
End Function