I'm new to VBA in Excel 2003. I've written a UDF that takes in two strings: one filter and one that will be a cell reference. When I am debugging and call the function a couple times in a row in the VBA editor, everything works as I expect. But when I use the UDF multiple times in a spreadsheet, all uses show the exact same answer, despite there being different input.
The function is below. It updates a pivot table and then copies the value out of a cell. But I must be misusing it:
Function UpdatePivotAndFetchCell(catcode As String, theCell As String) As Variant
Dim ws, pt, catField, pi, theval, finalVal
Set ws = Worksheets("Reporting")
Set pt = ws.PivotTables("MyReport")
pt.RefreshTable
Set catField = pt.PivotFields("Category")
For Each pi In catField.PivotItems
If InStr(pi.Value, catcode) Then
pt.PivotFields("Category").CurrentPage = pi.Value
theval = ws.Range(theCell).Value
On Error Resume Next
If (TypeName(theval) <> "Error") Then
finalVal = theval
End If
Exit For
End If
Next pi
UpdatePivotAndFetchCell = finalVal
End Function
If I run it with the VBA editor, I get:
Debug.Print (UpdatePivotAndFetchCell("C001", "K284"))
'Returns 0.48
Debug.Print (UpdatePivotAndFetchCell("C002", "K284"))
'Returns 0.52
But within a worksheet:
=UpdatePivotAndFetchCell("C001", "K284")
(displays 0.52)
=UpdatePivotAndFetchCell("C002", "K284")
(displays 0.52)
Am I not specifying the function properly, maybe? It's a mystery to me.
Functions called from a worksheet are not allowed to change sheets. Otherwise you could easily create infinite loops in the calculation tree. So everything that changes the sheet is ignored (such as RefreshTable() or .CurrentPage =) and causes the function to stop.
For completeness sake, there is a workaround, but you should not use it, and you may run into problems if you do.
Related
I am trying to use VBA for a UDF to manipulate its results based on the "Find" function as it's much quicker than polling every cell looking for results.
I have cut it right back to it's basics to reproduce the error and I still get "Object variable or With block variable not set" error on this part SearchRange.FindPrevious.Address (You have to put a stop on the line and step it to get the error, UDF's just quit out without returning the error outside of debug mode)
Here's the kicker, I get the error when trying to use the UDF in a sheet like so: =testfind("3206-1",E:E) however when I use it in the immediate window in the VBE like so: ?testfind("3206-1",Range("E:E")) i get the correct result of $E$15295 without error.
Code is:
Function TestFind(FindString As String, SearchRange As Range)
Dim ActiveAddress As Range
Set ActiveAddress = SearchRange.Find(FindString)
TestFind = SearchRange.FindPrevious.Address
End Function
Modified with L42's changes and now it works:
Function TestFind(FindString As String, SearchRange As Range)
Dim ActiveAddress As Range
Set ActiveAddress = SearchRange.Find(FindString)
TestFind = SearchRange.Find(FindString, , , , , xlPrevious).Address
End Function
Edit1:
This code
TestFind = SearchRange.FindPrevious.Address
and this as previously suggested:
TestFind = SearchRange.FindPrevious(ActiveAddress).Address
doesn't work when you use it as worksheet function as what Dan experienced.
Below doesn't directly answer the question but just an alternative.
Alternative: Change the SearchDirection argument in the Find method.
Function TestFind2(FindString As String, SearchRange As Range)
Dim ActiveAddress As Range
Set ActiveAddress = SearchRange.Find(FindString, , , , , xlPrevious)
TestFind2 = ActiveAddress.Address
End Function
Note: You need to add error handler to trap searches with no match found.
How to test:
Sub marine()
Debug.Print TestFind(4, [A:A])
End Sub
I created code to set the pagebreaks in an excel report to deal with the orphan issue (i.e. one line of text spills over onto the next page, etc.). The code works fine when I run it with the report open / visible.
It is part of a larger application which is opened and the code executed from MS Access. Excel is not visible during the process to improve performance.
When I run my code from MS Access it no longer works... it doesn't produce an error, but simply ignores the actual pagebreak setting command.
I read on various forums that in order to avoid this problem, excel needs to be first switched over to ActiveWindow.View = xlPageBreakPreview, but that doesn't work either (I suspect since Excel isn't visible).
I have tested for the following:
Code works when it is started manually or stepped through with F8
Code is executed when called upon from Access (I set breakpoints)
Switching the window view doesn't do anything either
How can I get Excel to change the pagebreaks via code when Excel is run in the background?
This is my code:
Sub TheOrphanProblem()
Dim iPageBrkRow
'Determine if there are page breaks and if so on which row of the document
If FindNthAutoPageBreak(wsRptHolding, 1) Is Nothing Then
'No pagebreak found so we exit the sub
Exit Sub
Else
iPageBrkRow = FindNthAutoPageBreak(wsRptHolding, 1).Row 'Get row
End If
Debug.Print iPageBrkRow
Dim x As Integer
Dim sCase As String
Dim rNewposition As Range
With wsRptHolding
'Code edited out for brevity. This part checks if there is an orphan problem and finds the new position for a pagebreak if needed.
It then provides that position as a range called "rNewposition".
'Moves page break to calculated position
ActiveWindow.View = xlPageBreakPreview
.HPageBreaks.Add rNewposition
ActiveWindow.View = xlNormalView
End With
End Sub
This is the code I use to find the pagebreak positions...
Private Function FindNthAutoPageBreak(Sht As Worksheet, Nth As Long) As Range
'Set page break of the last page so that sub asset groups are kept together
Dim HP As HPageBreak
Dim Ctr As Long
For Each HP In Sht.HPageBreaks
If HP.Type = xlPageBreakAutomatic Then
Ctr = Ctr + 1
If Ctr = Nth Then
Set FindNthAutoPageBreak = HP.Location
End If
End If
Next HP
End Function
Try this
ActiveSheet.DisplayPageBreaks = True
When I enter the following function as a UDF in a cell:
Function VisibleBlankCells(r As Range) As Long
On Error Resume Next
VisibleBlankCells = Intersect(r.SpecialCells(xlCellTypeVisible), r.SpecialCells(xlCellTypeBlanks)).Count
On Error GoTo 0
End Function
r.SpecialCells(xlCellTypeBlanks) evaluates ALL cells in r as empty regardless of whether they contain text or not. What might be the cause of this and an alternative solution?
Get rid of the On Error Resume Next for a start - you should always assume that your code will fail and account for it accordingly, simply ignoring errors will just complicate matters.
Secondly ,there is no need to use Intersect - just identify the visible cells directly, and then use a further SpecialCells() method to identify the blank child cells.
Function VisibleBlankCells(r As Range) As Long
VisibleBlankCells = r.SpecialCells(xlCellTypeVisible).SpecialCells(xlCellTypeBlanks).Count
End Function
tested with this:
Sub test_code()
Dim r As Range: Set r = Selection
Debug.Print CountBlanks(r)
End Sub
Function CountBlanks(r As Range) As Long
CountBlanks = r.SpecialCells(xlCellTypeVisible).SpecialCells(xlCellTypeBlanks).Count
End Function
This kind of filter mechanism won't work in an UDF (see this for information on that). I suggest a looping inside your UDF:
Public Function VisibleBlankCells(rng As Range) As Long
Dim i As Integer
Dim cell As Range
i = 0
For Each cell In rng
If cell.Rows.Hidden = False And _
cell.Columns.Hidden = False And _
cell.Value = "" Then
i = i + 1
End If
Next
VisibleBlankCells = i
End Function
However, there may be some problems regarding the updating and functionality:
The value of the UDF only updates after editing the referenced range or calling other UDFs. So if you hide a column or row in that range, it won't have an instant effect
In the (working) execution of your code in a Sub, the visible cells (also) refer to yet unused cells in your worksheet to be "not visible". In my solution however, all cells that are not contained in a hidden row/column are considered visible.
I have the following code:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim RR As Range
Dim TestArea As Range
Dim foremenList As Range
Dim workerList As Range
Dim workers As Range
Dim Foremen As Range
Dim i As Integer
Dim R As Range
Dim EmplList() As Variant
Set TestArea = Sheet90.Range("b4:q8", "b15:q19", "b26:q30")
Set foremenList = Sheet90.Range("V24:V30")
Set RR = Sheet90.Range("AA25:AA46")
i = 0
For Each R In RR.Cells
If Len(R.Value) > 0 Then
EmplList(i) = R.Value
i = i + 1
End If
Next R
Dim ValidStr As String
Set ValidStr = Join(EmplList, ",")
With Sheet90.Range("b26").Validation
.Delete
.Add xlValidateList, xlValidAlertStop, _
xlBetween, "1,2,3"
End With
Sheet90.Range("b40").Value = "Test"
End Sub
But when I press run to test it, it prompts me for a macro name.
Additionally, it does not trigger on Worksheet_Changeany more.
Is this an error (i.e. I forgot a semicolon or something) that consistently triggers Excel VBA to behave like this? If so, what should I look for in the future?
The reason you can't run this one with the Run Sub button is because it requires a parameter. If you want to run this standalone, one possibility is to run it in the Immediate Window so you can manually pass in the parameter. Since this one is expecting a more complex data type (range) you may want to create a small sub to call it so that you can properly create your range and pass that in. Then you can use the Run Sub on this sub which will call your other one.
As far is it not triggering on Worksheet_Change, I am not able to tell what is causing it just from what you posted. However, you do need to make sure that it is located on the code page for the worksheet you are trying to run it from. If you need the same one to run from multiple sheets, you should put it into a module and call it from each sheet's Worksheet_Change method.
You can't press F5 or the run button to run triggered code. You would have to make a change in the sheet where this code is located in order for the code to run. Also, if this code is not located in Sheet90, then you won't see anything happen because this code only makes changes to Sheet90. Lastly, to make sure events are enabled, you can run this bit of code:
Sub ReEnable_Events()
Application.EnableEvents = True
End Sub
Note that you will still have to enable macros.
The problem stems from two lines:
Set ValidStr = Join(EmplList, ",")
was not a valid use of the Set keyword (It's a string and not an object), and
Set TestArea = Sheet90.Range("b4:q8", "b15:q19", "b26:q30")
apparently has too many arguments.
According to Microsoft, it should be a single string argument like:
Set TestArea = Sheet90.Range("b4:q8, b15:q19, b26:q30")
Commenting both of these out made the code run fine both with the run sub button, and on the event.
The "Name Macro" dialog is some kind of error indicator, but I still don't know what it means, other than Code Borked
For a given cell, I select Data/Validation and set Allow to "List". I now wish to set Source like so:
=rNames(REGS)
but that does not work (name not found). So I go Insert/Name/Define and create "REGNAMES" by simply assigning the formula above (no cell range). I then return to the Data/Validation and when I set Source like so:
=REGNAMES
Now I get "Source currently evaluates to error". Unfortunately, this error does not go away even after I ignore it. I can create a range formula in the sheet like so:
{=REGNAMES}
and drag this to the right across a couple cells and the rNames function faithfully returns
Option #1 | Options #2 | ...
That is, the function returns a range as intended.
I know that I can use macro code to manipulate the List setting for that cell out of VBA. I don't like these side-effects much. I would prefer a clean dependency tree built on functions. Any ideas how to get the Data/Validation to accept the array values returned from rNames?
Thanks.
PS: rNames returns the result range as a Variant, if that has any bearing.
I think the problem is that data validation dialog only accepts the following "lists":
an actual list of things entered directly into the Source field
a literal range reference (like $Q$42:$Q$50)
a named formula that itself resolves to a range reference
That last one is key - there is no way to have a VBA function just return an array that can be used for validation, even if you call it from a named formula.
You can write a VBA function that returns a range reference, though, and call that from a named formula. This can be useful as part of the following technique that approximates the ability to do what you actually want.
First, have an actual range somewhere that calls your arbitrary-array-returning VBA UDF. Say you had this function:
Public Function validationList(someArg, someOtherArg)
'Pretend this got calculated somehow based on the above args...
validationList = Array("a", "b", "c")
End Function
And you called it from $Q$42:$Q$50 as an array formula. You'd get three cells with "a", "b", and "c" in them, and the rest of the cells would have #N/A errors because the returned array was smaller than the range that called the UDF. So far so good.
Now, have another VBA UDF that returns just the "occupied" part of a range, ignoring the #N/A error cells:
Public Function extractSeq(rng As Range)
'On Error GoTo EH stuff omitted...
'Also omitting validation - is range only one row or column, etc.
Dim posLast As Long
For posLast = rng.Count To 1 Step -1
If Not IsError(rng(posLast)) Then
Exit For
End If
If rng(posLast) <> CVErr(xlErrNA) Then
Exit For
End If
Next posLast
If posLast < 1 Then
extractSeq = CVErr(xlErrRef)
Else
Set extractSeq = Range(rng(1), rng(posLast))
End If
End Function
You can then call this from a named formula like so:
=extractSeq($Q$42:$Q$50)
and the named formula will return a range reference that Excel will accept an allowable validation list. Clunky, but side-effect free!
Note the use of the keyword 'Set' in the above code. It's not clear from your question, but this might be the only part of this whole answer that matters to you. If you don't use 'Set' when trying to return a range reference, VBA will instead return the value of the range, which can't be used as a validation list.
I was just doing some research on accessing the contents of a Shapes dropdown control, and discovered another approach to solving this problem that you might find helpful.
Any range that can have a validation rule applied can have that rule applied programmatically. Thus, if you want to apply a rule to cell A1, you can do this:
ActiveSheet.Range("A1").Validation.Add xlValidateList, , , "use, this, list"
The above adds an in-cell dropdown validation that contains the items "use," "this," and "list." If you override the Worksheet_SelectionChange() event, and check for specific ranges within it, you can call any number of routines to create/delete validation rules. The beauty of this method is that the list referred to can be any list that can be created in VBA. I needed a dynamically-generated list of an ever-changing subset of the worksheets in a workbook, which I then concatenated together to create the validation list.
In the Worksheet_SelectionChange() event, I check for the range and then if it matches, fire the validation rule sub, thus:
Private Sub Worksheet_SelectionChange(ByVal Target as Range)
If Target.Address = "$A$1" Then
UpdateValidation
End If
End Sub
The validation list-builder code in UpdateValidation() does this:
Public Sub UpdateValidation()
Dim sList as String
Dim oSheet as Worksheet
For Each oSheet in Worksheets
sList = sList & oSheet.Name & ","
Next
sList = left(sList, len(sList) -1) ' Trim off the trailing comma
ActiveSheet.Range("A1").Validation.Delete
ActiveSheet.Range("A1").Validation.Add xlValidateList, , , sList
End Sub
And now, when the user clicks the dropdown arrow, he/she will be presented with the updated validation list.
Sounds like your rNames function is probably returning a 1-dimensional array (which will be treated as a row).
Try making your function return a column as a 1-based 2-dimensional array (Ansa(1,1) then Ansa(2,1) etc)
Couln't you rather use dynamic range names ? That's quite easy and does not require any vba.
For the future:
Following is then used in a named range and the named range set as the 'Data Validation' 'List' value
Function uniqueList(R_NonUnique As Range) As Variant
Dim R_TempList As Range
Dim V_Iterator As Variant
Dim C_UniqueItems As New Collection
On Error Resume Next
For Each V_Iterator In R_NonUnique
C_UniqueItems.Add "'" & V_Iterator.Parent.Name & "'!" & V_Iterator.Address, CStr(V_Iterator.Value2)
Next V_Iterator
On Error GoTo 0
For Each V_Iterator In C_UniqueItems
If R_TempList Is Nothing Then
Set R_TempList = Range(V_Iterator)
End If
Set R_TempList = Union(R_TempList, Range(V_Iterator))
Next V_Iterator
Set uniqueList = R_TempList
End Function
#user5149293 I higly appreciate your code, but I recommend to prevent the collection from throwing an error, when adding duplicate values. The usage of a custom formula in the data validation list or in Name-Manager-Formula prevents the code from using the vbe debugger, which makes it very hard to trace back errors here (I ran into this problem myself, when using your code).
I recommend to check the existence of key in the collection with a separate function:
Function uniqueList(R_NonUnique As Range) As Variant
'Returns unique list as Array
Dim R_TempList As Range
Dim V_Iterator As Variant
Dim C_UniqueItems As New Collection
For Each V_Iterator In R_NonUnique
'Check if key already exists in the Collection
If Not HasKey(C_UniqueItems, V_Iterator.Value2) Then
C_UniqueItems.Add Item:="'" & V_Iterator.Parent.Name & "'!" & V_Iterator.Address, Key:=CStr(V_Iterator.Value2)
End If
Next V_Iterator
For Each V_Iterator In C_UniqueItems
If R_TempList Is Nothing Then
Set R_TempList = Range(V_Iterator)
End If
Set R_TempList = Union(R_TempList, Range(V_Iterator))
Next V_Iterator
Set uniqueList = R_TempList
End Function
Function HasKey(coll As Collection, strKey As String) As Boolean
'https://stackoverflow.com/questions/38007844/generic-way-to-check-if-a-key-is-in-a-collection-in-excel-vba
Dim var As Variant
On Error Resume Next
var = coll(strKey)
HasKey = (Err.Number = 0)
Err.Clear
End Function