speed up Excel off-sheet dependents search - vba

I've incorporated the off-sheet dependents search using the "ShowDependents" and "NavigateArrow" VBA methods. Everything works well but it is just painfully slow (for a large number of dependents).
Are there alternatives, way to speed it up? I've tried disabling the ScreenUpdating but that doesn't speed it up by much.
This is what my code is based on: http://www.technicana.com/vba-for-checking-dependencies-on-another-sheet

Consider the following function which is supposed to return true if the cell you pass it has a direct dependent on a different sheet:
Function LeadsOut(c As Range) As Boolean
Application.ScreenUpdating = False
Dim i As Long, target As Range
Dim ws As Worksheet
Set ws = ActiveSheet
c.ShowDependents
On Error GoTo return_false
i = 1
Do While True
Set target = c.NavigateArrow(False, i)
If c.Parent.Name <> target.Parent.Name Then
ws.Select
ActiveSheet.ClearArrows
Application.ScreenUpdating = True
LeadsOut = True
Exit Function
End If
i = i + 1
Loop
return_false:
LeadsOut = False
ActiveSheet.ClearArrows
Application.ScreenUpdating = True
End Function
Sub test()
MsgBox LeadsOut(Selection)
End Sub
To test it, I linked the test sub to a command button on Sheet1.
In A2 I entered the formula = A1 + 1, with no other formulas on Sheet1.
On Sheet2 I entered the formula =Sheet1!A2.
Back on Sheet1, if I select A2 and invoke the sub it almost instantly pops up "True". But if I select A1 and invoke the sub it returns "False" -- but only after a delay of several seconds.
To debug it, I put a Debug.Print i right before i = i + 1 in the loop. The Immediate Window, after running it again, looks like:
32764
32765
32766
32767
Weird!!!!!
I was utterly stumped until I replaced Debug.Print i by
Debug.Print target.Address(External:=True)
Which led to output that looks ends like:
[dependents.xlsm]Sheet1!$A$1
[dependents.xlsm]Sheet1!$A$1
[dependents.xlsm]Sheet1!$A$1
[dependents.xlsm]Sheet1!$A$1
NavigateArrow(False,i) goes back to the originating cell and stays there once i exceeds the number of dependents! This is seemingly undocumented and massively annoying. The code you linked to was written by someone who hasn't discovered this. As a kludge, you should check that when you are navigating arrows you haven't returned to the starting point. The following seems to work almost instantly in all cases, although I haven't tested it very much:
Function LeadsOut(c As Range) As Boolean
Application.ScreenUpdating = False
Dim i As Long, target As Range
Dim ws As Worksheet
Set ws = ActiveSheet
c.ShowDependents
On Error GoTo return_false
i = 1
Do While True
Set target = c.NavigateArrow(False, i)
If target.Address(External:=True) = c.Address(External:=True) Then
GoTo return_false
End If
If c.Parent.Name <> target.Parent.Name Then
ws.Select
ActiveSheet.ClearArrows
Application.ScreenUpdating = True
LeadsOut = True
Exit Function
End If
i = i + 1
Loop
return_false:
LeadsOut = False
ActiveSheet.ClearArrows
Application.ScreenUpdating = True
End Function
The key lines are the three lines which begin
If target.Address(External:=True) = c.Address(External:=True)
Adding some such check in the sub you linked to should make a massive difference.

Related

Macro fires 50% of the time when changing slicer item

I have a particular problem and couldn't find any solution anywhere on the internet.
So I have a pivot table which is connected to 6 slicers and also a chart which data range is dependent on pivot table values.
I've made a macro which updates chart scales everytime a value in any of the worksheet cells is changed. Here is the macro:
Public Sub worksheet_Change(ByVal Target2 As Range)
If ActiveSheet.Name = "Dashboard" Then
Application.ScreenUpdating = False
Application.EnableEvents = False
Application.DataEntryMode = xlOff
'Chart_axis Macro
Sheets("Dashboard").ChartObjects("Chart 9").Activate
If ActiveSheet.Range("B19") = "excluding CE" Then
ActiveChart.Axes(xlValue).MinimumScale = Range("E3").Value
ActiveChart.Axes(xlValue).MaximumScale = Range("E4").Value
Else
ActiveChart.Axes(xlValue).MinimumScale = Range("A3").Value
ActiveChart.Axes(xlValue).MaximumScale = Range("A4").Value
End If
ActiveChart.Refresh
ActiveSheet.Range("B18").Select
Application.EnableEvents = True
Application.ScreenUpdating = True
End If
End Sub
In order to work as intended i also had to made a function which reads the active elements of a slicer:
Public Function GetSelectedSlicerItems(SlicerName As String) As String
Application.Volatile
Set coll = New Collection
Dim cache As Excel.SlicerCache
Dim i As Integer
Set cache = ActiveWorkbook.SlicerCaches(SlicerName)
Dim sItem As Excel.SlicerItem
Dim result As String
For Each sItem In cache.SlicerItems
If sItem.Selected And sItem.HasData Then
'Debug.Print sItem.Name
'Debug.Print sItem.HasData
'GetSelectedSlicerItems = (sItem.Name)
coll.Add sItem.Name
End If
Next sItem
For i = 1 To coll.Count
'Debug.Print coll(i)
result = result & coll(i) & ", "
Next i
result = Left(result, Len(result) - 2)
GetSelectedSlicerItems = result
End Function
My problem is that while the value of the function always updates when the slicer item is changed, the macro only does it randomly about 50% of the time.
Screenshot of my report:
The formulas containing the selected slicer items function are on the top right.
So do you have any idea how to make it work 100% of the time?
Thanks in advance,
Alan
Edit: i forgot to add that it's only the issue if only one slicer is highlited. When i select multiple slicers (with ctrl+click) it always works.

Transferring comments from one worksheet to another without using clipboard

I have a VBA script which add's comments to a background worksheet, which is working great. The problem I am having is moving this to a front worksheet.
I can use copy and paste special xlPasteComments but this then really slows down the update process. I have included below a section of what will be repeating code. If I use values it does not include the comments (I left this in to show) and I have tried Dim separating them out but this just causes as error with object not being supported.
If ws.Range("B9") = ("January") Then
Dim a As Long
Dim b As Long
ws.Range("J8:AN51").Value = area.Range("E2:AI45").Value
'This brings up a 438 runtime error (object doesnt support this propery
or method)
a = ws.Range("J8:AN51").Comments
b = area.Range("E2:AI45").Comments
a = b
'area.Range("E2:AI45").Copy
'ws.Range("J8:AN51").PasteSpecial xlPasteComments
ws.Range("J62:AN63").Value = area1.Range("E47:AI48").Value
ws.Range("J55:AN55").Value = area.Range("E52:AI52").Value
I have checked on Google but it just keeps bringing up how to copy values within a cell, and what I am after is just the comments, (as the values are already copied)
My initial idea was to try to load all the comments in an VBA array and then use this comment array to write to the other worksheet.
So, I tried to adapt this technique from Chip Pearson's website that does exactly that but for cell values.
Unfortunatly, using .comment.text on a range with multiple cells won't return an array which means that this method won't work.
This means that in order to transfer the comments to the other sheet using VBA, you would need to go through all cells one by one in the range (as a collection perhaps). Although I'm sure this would work, it most likely won't be faster than using xlPasteComments.
I would then resolve to use the usual VBA techniques to make your macro run faster by deactivating certain settings like automatic calculation, screen updating and events. Here is an example of how I would implement it (including some error handling):
Sub Optimize_VBA_Performance_Example()
Const proc_name = "Optimize_VBA_Performance_Example"
'Store the initial setting to reset it at the end
Dim Initial_xlCalculation_Setting As Variant
Initial_xlCalculation_Setting = Application.Calculation
With Application
.Calculation = xlCalculationManual
.ScreenUpdating = False
.DisplayStatusBar = False
.EnableEvents = False
End With
On Error GoTo Error_handler
'Your code
'Restore initial settings (before exiting macro)
With Application
.Calculation = Initial_xlCalculation_Setting
.ScreenUpdating = True
.EnableEvents = True
.DisplayStatusBar = True
End With
Exit Sub
Error_handler:
'Restore initial settings (after error)
With Application
.Calculation = Initial_xlCalculation_Setting
.ScreenUpdating = True
.EnableEvents = True
.DisplayStatusBar = True
End With
'Display error message
Call MsgBox("Run-time error '" & Err.Number & "': " & Err.Description & vbNewLine & _
"While running: " & proc_name & vbNewLine, _
vbCritical, "Error")
End Sub
If you only care about the text of the comment (and not the formatting), you can use the Range.Comment.Text object to copy the comment text. The main difficulty arises in error handling whether or not the comment exists. Then just loop through all the cells in your source range and assign the comment to the destination range.
Sub copyComment(source As Range, dest As Range)
Dim t As String
' first set up error handling to exit the sub if the source cell doesn't have a comment
On Error GoTo ExitCopyComment
t = source.Comment.Text
' change error handling to go to next line
On Error Resume Next
' assign the text to an existing comment at the destination
' use this 1,1 offset (first cell in range) syntax to overcome parser
' issue about assignment to constant
dest(1, 1).Comment.Text = t
' if that produced an error then we need to add a comment
If (Err) Then
dest.AddComment t
End If
ExitCopyComment:
' clear error handling
On Error GoTo 0
End Sub
Sub test()
Dim cell As Range
Sheet1.Activate
' loop through all cells in source
For Each cell In Sheet1.Range("E47:AI48").Cells
' calculate destination range as offset from source cell
Call copyComment(cell, Sheet2.Cells(cell.Row + 15, cell.Column + 5))
Next cell
End Sub

If Range EntireRow.Hidden

I would really appreciate your help with following issue I am facing.
I want 3 rows to be hidden unless something is written in a specific cell.
If something is written in the specific cell I want the next row to become unhidden. And so it suppose to continue with the next two cells.
I have written the following code in th module, please guide me on how to solve this (what am I doing wrong?)
Sub InsertRow()
If Range("U6") <> "" Then
Rows("7").EntireRow.Hidden = False
Rows("8:9").EntireRow.Hidden = True
End If
End Sub
Not sure if this is what you want, but this goes through all used cells in col U and checks rows beneath
Public Sub InsertRow()
Dim targetCol As Range, itm As Range
Set targetCol = Worksheets("Sheet1").UsedRange.Columns(21) 'UsedRange starts at A1
Application.ScreenUpdating = False 'Update sheet name and column number
For Each itm In targetCol.Cells
itm.Offset(1).EntireRow.Hidden = (Len(itm.Value2) = 0)
Next
Application.ScreenUpdating = True
End Sub
Try this solution:
Sub InsertRow()
Rows("7").EntireRow.Hidden = Range("U6") = ""
Rows("8:9").EntireRow.Hidden = True
End Sub

VLookup On Another Workbook

Trying to make a macro that will take an input (a 13 digit number) from cell A7, A8, A9 and so forth until a blank cell is reached, and run a vlookup with this against another workbook.
However, I'm just getting the #N/A error and I can't work out why.
My current code:
Sub getData()
Application.EnableEvents = False
Application.ScreenUpdating = False
Application.DisplayAlerts = False
Application.Calculation = xlManual
Dim wb As Workbook, src As Workbook
Dim srcRange As Range
Dim InputString
Dim OutputString
Dim i As Long
Set wb = ActiveWorkbook
Set src = Workbooks.Open("D:\Files\test1.csv", True, True)
Set srcRange = src.Sheets(1).Range("A1:H1").End(xlDown)
i = 7
Do While wb.ActiveSheet.Cells(i, 1) <> ""
InputString = wb.Worksheets("Sheet 1").Cells(i, 1)
OutputString = Application.VLookup(InputString, srcRange, 3, False)
wb.Worksheets("Sheet 1").Cells(i, 2) = OutputString
i = i + 1
Loop
src.Close (False)
Application.EnableEvents = True
Application.ScreenUpdating = True
Application.DisplayAlerts = True
Application.Calculation = xlAutomatic
End Sub
I think it is worth noting that the reference I'm looking up ("InputString") is defined as custom format #0 in the "src" file. I don't really know if this matters too much, it should still be an integer?
Additionally, the "OutputString" could be either numbers or text, which is why I've purposefully let it undefined. I have tried defining it as 'Variant', 'String', and 'Integer' but that's not really changed anything.
Thanks.
Set srcRange = src.Sheets(1).Range("A1:H1").End(xlDown)
This will get you a range of one single cell, on column A, i.e. A20.
Doing a VLookup on a single cell is meaningless.
What you probably meant was this:
With Sheets(1)
Set srcRange = .Range("A1:H" & .Range("A1").End(xlDown).Row)
End With
This will set to something like A1:H20.
First you should change your src range which is actually set to grab only the bottom row, not the range from top to bottom. Try
Set wb = ActiveWorkbook
Set src = Workbooks.Open(""D:\Files\test1.csv", True, True)
With src.Sheets(1)
Set srcRange = .Range(.Range("A1"), .Range("H1").End(xlDown))
End With
Second, I don't believe the CSV file would support a 13 digit number except as text. VLOOKUP is very sensitive about text vs general vs number so if the new src range doesn't help, try converting the inputstring using CStr() first.
Good luck!

VBA Record date of row change in specific column

I'm trying to automatically update the "Updated" column of an excel spreadsheet when any cell of that specific row changes to today's date. I was able to do this by hard-coding where the "Updated" column header would be, however, it is now necessary to search for that column header as it may move.
The code I am trying to implement works but immediately gives me the error Automation error - The object invoked has disconnected from it's clients.
Any help would be appreciated. Here is the code I have currently:
Private Sub Worksheet_Change(ByVal Target As Range)
If Not Intersect(Target, Range("A:DX")) Is Nothing Then
Dim f As Range
Set f = ActiveSheet.Range("A1:DD1").Find("Updated", lookat:=xlWhole)
' f.Row = Range(Target).Row
If Not f Is Nothing Then
Range(Split(f.Address, "$")(1) & Target.Row).Value = Now
Else
MsgBox "'Updated' header not found!"
End If
End If
End Sub
You got into an endless loop.
Try this:
Private Sub Worksheet_Change(ByVal Target As Range)
If Not Intersect(Target, Range("A:DX")) Is Nothing Then
Dim f As Range
Set f = ActiveSheet.Range("A1:DD1").Find("Updated", lookat:=xlWhole)
' f.Row = Range(Target).Row
If f Is Nothing Then
MsgBox "'Updated' header not found!"
ElseIf Intersect(Target, f.EntireColumn) Is Nothing Then
Intersect(Target.EntireRow, f.EntireColumn).Value = Now
' Else
' MsgBox "We entered this function again because the row above updated the Updated column", vbInformation, "False alarm"
End If
End If
End Sub
To understand what happens,
Uncomment the else and MsgBox
Put a breakpoint on the MsgBox
When you hit it, press [ctrl]-L
In a case such as this, I run into far fewer problems when I simply loop through the available cells to find the column header. Using the .Find method also works, but is less "tunable" to my needs in a custom application.
Public Function FindColumn(header As String) As Long
Dim lastCol As Long
Dim headerCol As Long
Dim i As Long
Dim sh As Worksheet
Set sh = ThisWorkbook.Sheets("VTO2 Labor")
lastCol = sh.Cells(1, sh.Columns.Count).End(xlToLeft).Column
headerCol = 0
For i = 1 To lastCol
If sh.Cells(1, i).Value = header Then
headerCol = i
End If
Next i
FindColumn = headerCol
End Function
It isn't clear on whether the Updated column header could be in row 1 or if it will always be in row 1, just not in the same location.
Private Sub Worksheet_Change(ByVal Target As Range)
If Not Intersect(Target, Range("A:DX")) Is Nothing Then
On Error GoTo bm_SafeExit
'TURN OFF EVENTS IF YOU ARE GOING TO WRITE A VALUE INTO THE WORKSHEET!
Application.EnableEvents = False
Dim uCol As Long, f As Range
If Application.CountIf(Rows(1), "updated") Then
uCol = Application.Match("updated", Rows(1), 0)
For Each f In Intersect(Target, Range("A:DX"))
If f.Row > 1 Then _
Cells(f.Row, uCol) = Now
Next f
Else
MsgBox "'Updated' header not found!"
End If
End If
bm_SafeExit:
Application.EnableEvents = True
End Sub
That should survive multiple updates (e.g. when pasting values). The problem I see is that is the Updated column is being shifted around, presumably through inserting columns or the like, then the change routine is going to run.