In Excel 2016 VBA, I'm refreshing several queries like this:
MyWorkbook.Connections(MyConnectionName).Refresh
After the code is done, and no errors are encountered, I see that the hourglass icons for most of the queries are still spinning for several seconds.
Is it possible to check for success AFTER all the refreshes are completed? I'm concerned that my code isn't going to know if an error happens after the code finishes but before the queries are done refreshing.
BTW I don't want to do a RefreshAll, because some of the queries are dependent on others (uses them as a source). I refresh them in a certain sequence so that dependent queries are refreshed after the queries they are dependent on.
UPDATE:
I see that the Connection objects have a read-only RefreshDate property, which at first glance looked like it could be used to do this check:
MyWorkbook.Connections(MyConnectionName).OLEDBConnection.RefreshDate
HOWEVER, it doesn't seem to be getting set. I get an error trying to check it. If I set a Variant variable to that RefreshDate property, the variable shows as "Empty". The source is a SQL server database.
The QueryTable object exposes two events: BeforeRefresh and AfterRefresh.
You need to change your paradigm from procedural/imperative to event-driven.
Say you have this code in ThisWorkbook (won't work in a standard procedural code module, because WithEvents can only be in a class):
Option Explicit
Private WithEvents table As Excel.QueryTable
Private currentIndex As Long
Private tables As Variant
Private Sub table_AfterRefresh(ByVal Success As Boolean)
Debug.Print table.WorkbookConnection.Name & " refreshed. (success: " & Success & ")"
currentIndex = currentIndex + 1
If Success And currentIndex <= UBound(tables) Then
Set table = tables(currentIndex)
table.Refresh
End If
End Sub
Public Sub Test()
tables = Array(Sheet1.ListObjects(1).QueryTable, Sheet2.ListObjects(1).QueryTable)
currentIndex = 0
Set table = tables(currentIndex)
table.Refresh
End Sub
The tables variable contains an array of QueryTable objects, ordered in the order you wish to refresh them; the currentIndex variable points to the index in that array, for the QueryTable you want to act upon.
So when Test runs, we initialize the tables array with the QueryTable objects we want to refresh, in the order we want to refresh them.
The implicit, event-driven loop begins when table.Refresh is called and the QueryTable fires its AfterRefresh event: then we report success, and update the event-provider table object reference with the next QueryTable in the array (only if the refresh was successful), and call its Refresh method, which will fire AfterRefresh again, until the entire array has been traversed or one of them failed to update.
Just found this solution at Execute code after a data connection is refreshed
The bottom line is: Excel refreshes data connection in the background and thus the rest of the code is executed without interruption.
Solution: set BackgroundQuery property to False
Example:
For Each cnct In ThisWorkbook.Connections
cnct.ODBCConnection.BackgroundQuery = False
Next cnct
Possible problem: don't know which connection it is...
Remedy: case... when...
Dim cnct as WorkbookConnection ' if option explicit
' ODBC and OLE DB
For Each cnct In ThisWorkbook.Connections
Select case cnct.type
case xlconnectiontypeodbc
cnct.ODBCConnection.BackgroundQuery = False
case xlconnectiontypeoledb
cnct.OledbConnection.BackgroundQuery = False
end select
Next cnct
As you can see, code above only deals with ODBC and OLE DB. Depending on what types of data connection you are using, you can expand the select case clause. Unless changed, once run, connection's BackgroundQuery will remain off.
Related
I have a really basic problem with my projects and I would like to know which approach is the best. I like to use (hated) globals, only for a few the most important objects in a workbook.
I am declaring e.g. my data tables in a such way:
'#Folder("Main")
Option Exclicit
Public tblDatabase As Listobject
Public tblReport As Listobject
Sub setMyTables()
Set tblDatabase = wsDatabase.ListObjects("tDatabase")
Set tblReport = wsReport.ListObjects("tReport")
End Sub
In the past I used this macro before actions on the table, e.g.:
Function getIdFromDatabaseTable() As Variant
' set variable-object to use
setMyTables <-- I used to table-setting-sub in every
macro which requires one of my table
' get ID from table
Dim arr As Variant
arr = tblDatabase.ListColumns("ID").DataBodyRange.Value2
' assign array to function result
getIdFromDataTable = arr
End Function
But why I had to begin almost every macro with calling setMyTables() macro? So I've started to use workbook open event to set my object variables:
[code in ordinary Module]
'#Folder("Main")
Option Exclicit
Public tblDatabase As Listobject
Public tblReport As Listobject
And call setMyTables() macro in Workbook_Open() event code. And here my problem is:
[TLTR] Setting variable-objects in Workbook-Open event seems unrielable. It seems it is not firing sometimes. I am sure that no macro error would reset the project and 'clear' already set variables, because sometimes it throws error on the very first macro run. It is not working occasionally and I don't know what pattern behind it is, I send Excel workbooks to my clients, and it's hard to debug what's realy going on there.
Additional comments
I've just read that this could happen if file is not in trusted localizations, I would like get to know best approach to handle declaring the most used objects globally (if possible without modifying someones trusted folders or another local-PC settings).
I know that I can set a 'flag' bool variable such as wasWorkbookOpenEventFired, but I would have to call checking function or make ifs on almost every Sub or Function in a workbook. So I think it isn't good solution too. Thanks for hints!
You'd have more robust results if you define public functions which each return a specific table, and use those instead of global variables:
Function DatabaseTable() As ListObject
Static rv As ListObject '<< cache the table here
'if your code gets reset then this will just re-cache the table
If rv Is Nothing then Set rv = wsDatabase.ListObjects("tDatabase")
Set DatabaseTable = rv
End Function
I am using a set of scripts that pull a lot of different data from iSeries via ODBC.
ActiveWorksheets.RefreshAll does not work as it doesn't leave enough time to run the background queries
I've tried the below,but to no avail
Dim qry As Connections
'Set qry =
For Each qry In ActiveWorksheets.Connections
qry.BackgroundQuery = False
qry.RefreshAll
DoEvents
Next qry
This gives me the expected Error 424 Object expected.
I don't expect to use Set qry = here, as I need to run through 30 different connections
Let's just call them connection1, connection2 etc for now, as their names are all over the place
Is the simplest option to stop the background query, refresh, activate background query - before the data import, or is there a better way?
I've looked all over SO - but can't find info on multiple ODBC connections
EDIT:
Dim qry As WorkbookConnection
For Each qry In ActiveWorkbook.Connections
qry.Refresh
DoEvents
Next qry
I believe your
Dim qry As Connections
should read
Dim qry As WorkbookConnection
The ActiveWorksheets.Connections.Item property returns an object of type WorkbookConnection. If you are trying to refresh the connections one at a time as it seems from your For Each statement, that object represents a single connection with methods like Refresh rather than the collection of all connections.
I managed to work this out. So all who may need this in the future can see:
Dim qry As WorkbookConnection
For Each qry In ActiveWorkbook.Connections
qry.ODBCConnection.BackgroundQuery = False
qry.Refresh
qry.ODBCConnection.BackgroundQuery = True
Next qry
Although it doesn't look like BackgroundQuery = True/False doesn't look like it's vital here. Turning it off means when you qry.Refresh, it pulls the data and refreshes it.
Also using For Each qry means that rather than writing out out 20 times, I can just look every connection and turn them off, refresh, and turn them back on
I came across this similar issue and read the replies: Modeless form that still pauses code execution
I have been attempting to apply in my own situation the suggestion provided by David Zemens. In my situation, I cannot seem to find an approach that incorporates Mr. Zemen's suggestion without also utilizing a GoTo.
I am wondering if there is a better or more elegant solution.
Here is an outline of what I am doing:
I have a UserForm with a Command Button that begins the code execution that will perform several actions on multiple Excel workbooks. As such, there are a number of blocks of code and the successful completion of one block of code allows for the execution of the subsequent block of code.
At a certain point, depending on the situation, the code might require User input; in other situations, the needed data is obtainable from an Excel. If input is needed from the User, another UserForm is displayed.
The User may need to view several different Excel sheets before entering the input, so the UserForm is modeless. So the code comes to a stop until the User enters the needed input and clicks another Command Button.
It is at this point I am having trouble: how to resume the program flow. Is the only way to 'pick-up where it left-off' is by using a GoTo statement? Or is there some way to organize the modules so there is a single consistent program flow, defined in one spot and not duplicated from the point at which User input might be needed?
Here is my take on the problem . Hope I understood the problem correctly.
Assumptions:
There are two user forms.
UserForm1 with a button to start the processing.
UserForm2 with a button to supply intermediate input.
A sub inside a module to start/ launch UserForm1.
VBA Code (for the sub routine)
Sub LaunchUserForm1()
Dim frm As New UserForm1
'/ Launch the main userform.
frm.Show vbModeless
End Sub
VBA Code (for UserForm1)
Private Sub cmdStart_Click()
Dim i As Long
Dim linc As Long
Dim bCancel As Boolean
Dim frm As UserForm2
'/ Prints 1 to 5 plus the value returned from UserForm2.
For i = 1 To 5
If i = 2 Then
Set frm = New UserForm2
'/ Launch supplementary form.
frm.Show vbModeless
'<< This is just a PoC. If you have large number of inputs, better way will be
' to create another prop such as Waiting(Boolean Type) and then manipulate it as and when User
' supplies valid input. Then validate the same in While loop>>
'/ Wait till we get the value from UserForm2.
'/ Or the User Cancels the Form with out any input.
Do While linc < 1 And (linc < 1 And bCancel = False)
linc = frm.Prop1
bCancel = frm.Cancel
DoEvents
Loop
Set frm = Nothing
End If
Debug.Print i + linc
Next
MsgBox "User Form1's ops finished."
End Sub
VBA Code (for UserForm2)
Dim m_Cancel As Boolean
Dim m_prop1 As Long
Public Property Let Prop1(lVal As Long)
m_prop1 = lVal
End Property
Public Property Get Prop1() As Long
Prop1 = m_prop1
End Property
Public Property Let Cancel(bVal As Boolean)
m_Cancel = bVal
End Property
Public Property Get Cancel() As Boolean
Cancel = m_Cancel
End Property
Private Sub cmdlinc_Click()
'/Set the Property Value to 10
Me.Prop1 = 10
Me.Hide
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
'/ Diasble X button
Me.Cancel = True
Me.Hide
Cancel = True
End Sub
OK so here are my thoughts.
You have a userform frmSelectUpdateSheet which you wish to use in order to allow the user to select the sheet, when the sheet can't be determined programmatically. The problem is that if you do .Show vbModeless (which allows the user to navigate the worksheet/s), then code continues to execute which either leads to errors or otherwise undesired output.
I think it's possible to adapt the method I described in the previous answer. However, that's out of the question here unless you're paying me to reverse engineer all of your code :P
Assuming you have a Worksheet object variable (or a string representing the sheet name, etc.) which needs to be assigned at this point (and that this variable is Public in scope), just use the CommandButton on the form to assign this based on the selected item in the frmSelectUpdateSheet list box.
This is probably a superior approach for a number of reasons (not the least of which is trying to avoid application redesign for this sort of fringe case), such as:
This keeps your form vbModal, and does prevent the user from inadvertently tampering with the worksheet during the process, etc.
Using this approach, the thread remains with the vbModal displayed frmSelectUpdateSheet, and you rely on the form's event procedures for control of process flow/code execution.
It should be easier (and hence, cheaper) to implement; whether you're doing it yourself or outsourcing it.
It should be easier (and hence, cheaper) to maintain.
NOW, on closer inspection, it looks like you're already doing this sort of approach with the cmdbtnSelect_Click event handler, which leads me to believe there's a related/follow-up problem:
The sheet names (in listbox) are not sufficient for user to identify the correct worksheet. So if the user needs the ability to "scroll" the sheet (e.g., to review data which does not fit in the window, etc.), then add some spinner buttons or other form controls to allow them to navigate the sheet.
Preface
I'm making this question specific to conform to SO asking guidelines, but feel free to suggest wholesale redesign if you wish. I may be using some bad practices.
Basic Question
I use ADO to execute a multi-step SQL Server query that takes several minutes to execute. I use Raiserror in my tsql queries to let myself know more verbosely which steps have finished. Is it possible to pass these messages to VBA before the complete query finishes, while still continuing with the query?
Details and Code
I use the vba below to execute the t-SQL query underneath. As you can see, there are two errors raised in the t-SQL that display "Step 1 complete" and "Step 2 complete". Could I pass these messages (or alternately use error numbers and pass those) back to VBA in a way that would allow me to detect them and update a progress bar while continuing to execute the query?
VBA used to execute the query:
Set cmd = New ADODB.Command
cmd.ActiveConnection = cnn
cmd.CommandTimeout = 0
cmd.CommandText = strQuery
Set rst = New ADODB.Recordset
rst.Open cmd
'Go to the second to last recordset of the multi-step query
String1 = Replace(strQuery, ";", "")
For Loop2 = 1 To (Len(strQuery) - (Len(String1) + 1))
Set rst = rst.NextRecordset
Next Loop2
'Copy results
If Not rst.EOF Then
(snip - actions)
Else
MsgBox "Error: No records returned."
End If
Stripped-down piece of multi-step tSQL query:
--#DRS1: The numbers being researched
select distinct numbers
into #DRS1
from Table1 (nolock)
where numbers in ()
--#DRS1: Index
create nonclustered index Idx_DRS1
on #DRS1(numbers);
Raiserror(“Step 1 complete”,1,1) with nowait;
--#DRS2: Table2 for numbers being researched
select distinct
DRS1.numbers
,a.ID
into #DRS2
from #DRS1 DRS1
join Table2 (nolock) a
on DRS1.numbers = a.numbers
Raiserror(“Step 2 complete”,1,1) with nowait;
--MORE STEPS
(more steps)
(more raiserror statements)
Clarification
I am not interested in:
A method that doesn't allow me to update a progress bar until the query is completely done.
A method that uses Progress/MaxProgress, because as I understand it that would return separate numbers for each of the steps in my query, rather than one progress measure for the entire query.
I am less interested in:
Using # records affected messages to determine progress, because some steps may return equal numbers of records to previous steps.
Research
The closest thing I have found to what I'm looking for is here, but as the discussion of that solution here says:
This approach would only work for stored procedures that are not intended to return results, say procs that insert data into tables. Another approach would be needed if your stored proc returns a result set.
Since I return results in the final step of my query to be manipulated in Excel I don't think this would work for me.
External link code for reference
SQL:
CREATE PROCEDURE dbo.updTesting As
Declare #RetVal integer
Exec #RetVal = updTesting2
Return #RetVal
GO
CREATE PROCEDURE dbo.updTesting2 As
raiserror('Error From Testing 2 procedure',16,1)
Return -2
GO
VBA:
Private Sub Command1_Click()
On Error GoTo ErrorHandler
Dim db As ADODB.Connection
Dim cmd As ADODB.Command
Set db = New ADODB.Connection
db.CursorLocation = adUseClient
db.Open "provider=sqloledb;data source=handel;initial catalog=northwind;integrated security=sspi"
Set cmd = New ADODB.Command
With cmd
Set .ActiveConnection = db
.CommandText = "updTesting"
.CommandType = adCmdStoredProc
.Parameters.Append .CreateParameter("#RetVal", adInteger, adParamReturnValue)
.Execute , , adExecuteNoRecords
End With
ExitPoint:
On Error Resume Next
Set cmd.ActiveConnection = Nothing
Set cmd = Nothing
db.Close
Set db = Nothing
Exit Sub
ErrorHandler:
MsgBox "Error # " & Err.Number & vbNewLine & vbNewLine & Err.Description
Resume ExitPoint
End Sub
There are several possibilities to work out a solution for your problem:
(1) Capture the error messages as they occur while the query is running. That's the requested approach.
(2) Break-down the big, long query into several smaller chunks and run them one after the other. Like this you know which part is completed and you can update your progress bar based on that information just before sending the next chunk to the server.
(3) Update the big, long query to log its progress on the server in a temp table and then read out this log while the other query is still running.
While I'd recommend to use errors only when errors occur and not to "abuse" them for logging, tracking, or feedback, both options (1 & 2) are quite feasible with events:
Similar to Worksheet events Worksheet_Change, Worksheet_Activate, or Worksheet_BeforeDoubleClick there are also ADODB events for ADODB.Connection and ADODB.Recordset. Both are well documented and can be easily viewed within the VBE by (1) adding a reference to Microsoft ActiveX Data Objects x.x Library (2) pressing F2 (3) selecting the ADODB library in the drop-down menu at the top (4) and finally looking up Recordset or Connection within the classes. Here are the available events for Connection:
As you can see, all events are marked with a lightning. To capture / use these events you need to create a Class Module in the VBE and add the following line to it:
Dim WithEvents adoConnection As ADODB.Connection
Afterwards, you can make use of the newly created ADODB.Connection event and select the required event from the top of the list:
The applicable event for option (1) is the InfoMessage event which occurs "[...] whenever a warning occurs during a ConnectionEvent operation." The import part here is during a connection. So, this event fires automatically whenever an ADODB connection "gets" an error.
Of course, this means that the original query to the server must be sent without waiting for an answer. Instead you should use the above event to capture any errors while the query is executing and create yet another event to automatically fire when the entire query completed.
For some more help in respect to asynchronous ADODB connection and possible problems with them you might want to have a look at the following two posts here:
ExecuteComplete ADODB Connection event not fired with adAsyncExecute parameter
Running multiple async queries with ADODB - callbacks not always firing
A similar approach can be used with option (3) as described above and asynchronous ADODB connections.
Let me know if this solves your problems or if you have any further questions.
All available ADODB events can be reviewed here https://msdn.microsoft.com/en-us/library/ms675083%28v=vs.85%29.aspx
I have created a VSTO plugin with my own RTD implementation that I am calling from my Excel sheets. To avoid having to use the full-fledged RTD syntax in the cells, I have created a UDF that hides that API from the sheet.
The RTD server I created can be enabled and disabled through a button in a custom Ribbon component.
The behavior I want to achieve is as follows:
If the server is disabled and a reference to my function is entered in a cell, I want the cell to display Disabled.
If the server is disabled, but the function had been entered in a cell when it was enabled (and the cell thus displays a value), I want the cell to keep displaying that value.
If the server is enabled, I want the cell to display Loading.
Sounds easy enough. Here is an example of the - non functional - code:
Public Function RetrieveData(id as Long)
Dim result as String
// This returns either 'Disabled' or 'Loading'
result = Application.Worksheet.Function.RTD("SERVERNAME", "", id)
RetrieveData = result
If(result = "Disabled") Then
// Obviously, this recurses (and fails), so that's not an option
If(Not IsEmpty(Application.Caller.Value2)) Then
// So does this
RetrieveData = Application.Caller.Value2
End If
End If
End Function
The function will be called in thousands of cells, so storing the 'original' values in another data structure would be a major overhead and I would like to avoid it. Also, the RTD server does not know the values, since it also does not keep a history of it, more or less for the same reason.
I was thinking that there might be some way to exit the function which would force it to not change the displayed value, but so far I have been unable to find anything like that.
EDIT:
Due to popular demand, some additional info on why I want to do all this:
As I said, the function will be called in thousands of cells and the RTD server needs to retrieve quite a bit of information. This can be quite hard on both network and CPU. To allow the user to decide for himself whether he wants this load on his machine, they can disable the updates from the server. In that case, they should still be able to calculate the sheets with the values currently in the fields, yet no updates are pushed into them. Once new data is required, the server can be enabled and the fields will be updated.
Again, since we are talking about quite a bit of data here, I would rather not store it somewhere in the sheet. Plus, the data should be usable even if the workbook is closed and loaded again.
Different tack=new answer.
A few things I've discovered the hard way, that you might find useful:
1.
In a UDF, returning the RTD call like this
' excel equivalent: =RTD("GeodesiX.RTD",,"status","Tokyo")
result = excel.WorksheetFunction.rtd( _
"GeodesiX.RTD", _
Nothing, _
"geocode", _
request, _
location)
behaves as if you'd inserted the commented function in the cell, and NOT the value returned by the RTD. In other words, "result" is an object of type "RTD-function-call" and not the RTD's answer. Conversely, doing this:
' excel equivalent: =RTD("GeodesiX.RTD",,"status","Tokyo")
result = excel.WorksheetFunction.rtd( _
"GeodesiX.RTD", _
Nothing, _
"geocode", _
request, _
location).ToDouble ' or ToString or whetever
returns the actual value, equivalent to typing "3.1418" in the cell. This is an important difference; in the first case the cell continues to participate in RTD feeding, in the second case it just gets a constant value. This might be a solution for you.
2.
MS VSTO makes it look as though writing an Office Addin is a piece of cake... until you actually try to build an industrial, distributable solution. Getting all the privileges and authorities right for a Setup is a nightmare, and it gets exponentially worse if you have the bright idea of supporting more than one version of Excel. I've been using Addin Express for some years. It hides all this MS nastiness and let's me focus on coding my addin. Their support is first-rate too, worth a look. (No, I am not affiliated or anything like that).
3.
Be aware that Excel can and will call Connect / RefreshData / RTD at any time, even when you're in the middle of something - there's some subtle multi-tasking going on behind the scenes. You'll need to decorate your code with the appropriate Synclock blocks to protect your data structures.
4.
When you receive data (presumably asynchronously on a separate thread) you absolutely MUST callback Excel on the thread on which you were intially called (by Excel). If you don't, it'll work fine for a while and then you'll start getting mysterious, unsolvable crashes and worse, orphan Excels in the background. Here's an example of the relevant code to do this:
Imports System.Threading
...
Private _Context As SynchronizationContext = Nothing
...
Sub New
_Context = SynchronizationContext.Current
If _Context Is Nothing Then
_Context = New SynchronizationContext ' try valiantly to continue
End If
...
Private Delegate Sub CallBackDelegate(ByVal GeodesicCompleted)
Private Sub GeodesicComplete(ByVal query As Query) _
Handles geodesic.Completed ' Called by asynchronous thread
Dim cbd As New CallBackDelegate(AddressOf GeodesicCompleted)
_Context.Post(Function() cbd.DynamicInvoke(query), Nothing)
End Sub
Private Sub GeodesicCompleted(ByVal query As Query)
SyncLock query
If query.Status = "OK" Then
Select Case query.Type
Case Geodesics.Query.QueryType.Directions
GeodesicCompletedTravel(query)
Case Geodesics.Query.QueryType.Geocode
GeodesicCompletedGeocode(query)
End Select
End If
' If it's not resolved, it stays "queued",
' so as never to enter the queue again in this session
query.Queued = Not query.Resolved
End SyncLock
For Each topic As AddinExpress.RTD.ADXRTDTopic In query.Topics
AddinExpress.RTD.ADXRTDServerModule.CurrentInstance.UpdateTopic(topic)
Next
End Sub
5.
I've done something apparently akin to what you're asking in this addin. There, I asynchronously fetch geocode data from Google and serve it up with an RTD shadowed by a UDF. As the call to GoogleMaps is very expensive, I tried 101 ways and several month's of evenings to keep the value in the cell, like what you're attempting, without success. I haven't timed anything, but my gut feeling is that a call to Excel like "Application.Caller.Value" is an order of magnitude slower than a dictionary lookup.
In the end I created a cache component which saves and re-loads values already obtained from a very-hidden spreadsheet which I create on the fly in Workbook OnSave. The data is stored in a Dictionary(of string, myQuery), where each myQuery holds all the relevant info.
It works well, fulfils the requirement for working offline and even for 20'000+ formulas it appears instantaneous.
HTH.
Edit: Out of curiosity, I tested my hunch that calling Excel is much more expensive than doing a dictionary lookup. It turns out that not only was the hunch correct, but frighteningly so.
Public Sub TimeTest()
Dim sw As New Stopwatch
Dim row As Integer
Dim val As Object
Dim sheet As Microsoft.Office.Interop.Excel.Worksheet
Dim dict As New Dictionary(Of Integer, Integer)
Const iterations As Integer = 100000
Const elements As Integer = 10000
For i = 1 To elements + 1
dict.Add(i, i)
Next
sheet = _ExcelWorkbook.ActiveSheet
sw.Reset()
sw.Start()
For i As Integer = 1 To iterations
row = 1 + Rnd() * elements
Next
sw.Stop()
Debug.WriteLine("Empty loop " & (sw.ElapsedMilliseconds * 1000) / iterations & " uS")
sw.Reset()
sw.Start()
For i As Integer = 1 To iterations
row = 1 + Rnd() * elements
val = sheet.Cells(row, 1).value
Next
sw.Stop()
Debug.WriteLine("Get cell value " & (sw.ElapsedMilliseconds * 1000) / iterations & " uS")
sw.Reset()
sw.Start()
For i As Integer = 1 To iterations
row = 1 + Rnd() * elements
val = dict(row)
Next
sw.Stop()
Debug.WriteLine("Get dict value " & (sw.ElapsedMilliseconds * 1000) / iterations & " uS")
End Sub
Results:
Empty loop 0.07 uS
Get cell value 899.77 uS
Get dict value 0.15 uS
Looking up a value in a 10'000 element Dictionary(Of Integer, Integer) is over 11'000 times faster than fetching a cell value from Excel.
Q.E.D.
Maybe... Try making your UDF wrapper function non-volatile, that way it won't get called unless one of its arguments changes.
This might be a problem when you enable the server, you'll have to trick Excel into calling your UDF again, it depends on what you're trying to do.
Perhaps explain the complete function you're trying to implement?
You could try Application.Caller.Text This has the drawback of returning the formatted value from the rendering layer as text, but seems to avoid the circular reference problem.Note: I have not tested this hack under all possible circumstances ...