Setup:
After using any technique to wait for a Power Query to finish refreshing as described in the "Things I have tried:" section shown below, a message box can be displayed and any other code can be executed before the Power Query tables have finished updating according to the refreshing indicator (spinner?) icon shown in the Queries and Connections pane.
The exception to the above statement is the OnTime
method of the Application class, shown in the "Code" section below, which doesn't appear to interrupt the polling for a power query refresh. The problem is that it uses a hard coded amount of time to pause VBA code, and this will not always work since the size, amount, and duration of data being queried will change over time.
I tried:
DoEvents
, BackgrgoundQuery = False
, and CalculateUntilAsyncQueriesDone
methods and properties..
Code:
Private Sub sht_sub_Refresh_AllConnections_dev()
'Name: sht_sub_Refresh_AllConnections_dev
'Purpose: An attempt at using VBA to wait for Queries to finish updating before displaying a message.
'Description: Waits for a hard coded period of time before dislpaying the message box.
'State: WIP.
'Dev: Needs a way to look at the connection stream to somehow detect when its finished.
'DECLARATIONS:
'------------'
Dim procName As String 'Stores this procedure's name.
Dim qTblLst As QueryTables 'A query table collection object.
Dim qTblObj As QueryTable 'A query table object.
Dim conLst As Connections 'A connection collection object.
Dim conObj As WorkbookConnection 'A connection object.
Dim idx As Long 'A loop counter.
'INITIALIZATIONS:
'---------------'
procName = "sht_sub_Refresh_AllConnections_dev" 'Store this procedure's name.
Linit.ini_Setup_Project 'Setup the project if needed.
Set conLst = ThisWorkbook.Connections 'Set the connections list object.
Set conObj = conLst.Item(conLst.Count) 'Set an initial connection object.
idx = 0 'As an exit if the do loop continues without end.
'MAIN CODE BODY:
'--------------'
'Turn off backgroundquery for each connection type.
For Each conObj In conLst 'For each connection object,
With conObj
Select Case .Type 'Check the connection type,
Case 1 'If its an OLEDB connection then,
.OLEDBConnection.BackgroundQuery = False 'Set it's backgroundquery property to false.
Case 2 'If its an ODBC connection the,
.ODBCConnection.BackgroundQuery = False 'Set it's backgroundquery property to false.
End Select
End With
Next conObj
ThisWorkbook.RefreshAll 'Refresh all connections.
'DEV: Using loops, DoEvents and a query name starting with the letters "zzzz" as suggsted here:
'https://social.technet.microsoft.com/Forums/en-US/bc3f7748-8a52-498d-951c-4566b8adf45a/in-excel-2016-power-queries-dont-refresh-in-the-background-anymore?forum=powerquery
'and here:
'https://www.myonlinetraininghub.com/excel-forum/vba-macros/pause-macro-until-power-queries-finished-refreshing
'Attempt to wait until the last connection has finished refreshing.
Do Until Linit.gvTbl_ZZZZZ.QueryTable.Refreshing = True 'Wait until the last table starts refreshing,
idx = idx + 1 'Icrement a loop count,
If idx > 3000 Then Exit Do 'If the loop goes longer then 3000 iterations exit,
Loop 'otherwise continue waiting.
VBA.DoEvents 'Do events before continueing (doens't work).
Do Until Linit.gvTbl_ZZZZZ.QueryTable.Refreshing = False 'Wait until the last table finishes refreshing,
idx = idx + 1 'Icrement a loop count,
If idx > 3000 Then Exit Do 'If the loop goes longer then 3000 iterations exit,
Loop 'otherwise continue waiting.
VBA.DoEvents 'Do events before continueing (doens't work).
'DEV: The following is an attempt to get connections to
' finish refreshing before code continues as suggested here:
'https://stackoverflow.com/questions/22083668/wait-until-activeworkbook-refreshall-finishes-vba
Application.CalculateUntilAsyncQueriesDone 'This is placed here as well as after the refresh.
VBA.DoEvents 'Do events before continueing (doens't work).
Application.EnableEvents = False 'Maybe turning off events helps? (nope...),
Application.ScreenUpdating = False 'This is reset in the procedure called as an argument to the next line:
Application.OnTime DateAdd("s", 3, Now), _
"Lwksh.sht_sub_Msg_RefreshDone" 'The called procedure just displays a message box.
Application.EnableEvents = True 'Restore events,
Application.ScreenUpdating = True 'Restore screen updating.
'MEMORY CLEANUP:
'--------------'
EXIT_CLEAN:
procName = Empty
Set qTblLst = Nothing
Set qTblObj = Nothing
Set conLst = Nothing
Set conObj = Nothing
idx = 0
End Sub
Code notes:
Questions:
If your dataset refresh takes a long time because you have applied a set of heavy data transformations in Power Query, then what you can do instead is to push that set of heavy transformations to a dataflow.
If your queries are slow, you can enable the fast data load option to speed them up. Go to the Data tab and press the Get Data button, then open the Query Options. What is this? Go to the Global Data Load settings and check the Fast Data Load box.
Once the Power Query connection string is successfully configured, you can schedule data refresh for Excel Workbooks containing the Power Query connection on the Power BI site.
Finally, add data to your data source and then refresh it in Excel. Power Query automatically applies each transformation you created. This means you only need to create a query once and then you can run it whenever you want.
I understand your pain @neurojelly. I have been there. But as it turns out the solution is quite simple and is not using VBA.
In the Query properties window, you need to uncheck the "Enable background refresh" and then use DoEvents
.
I know for sure this works as I have been using this method for over a year now.
Please find the link to a sample file that has the code in it.
https://drive.google.com/uc?export=download&id=1ZLxSMEXPLda3QhaQoTyGGv3_sC-tpN-X
As for your second question, it is possible to use Iferror/OnEror
method's to detect if a query returns an error, but it doesn't necessarily check for errors in the query. It identifies if the query itself is returning an error pop-up which is skipped by default while running the VBA code. This method works most of the time, but not always.
Here is a workaround
Sub MyProcedure()
'
' Some procedures
'
Call ActiveWorkbook.RefreshAll
Call NotifyWhenRefreshComplete
End Sub
Private Sub NotifyWhenRefreshComplete()
Const PulseTimer As Currency = TimeValue("00:00:01")
Dim b1 As Boolean, b2 As Boolean
b1 = Sheet1.Range("ListObject1").ListObject.QueryTable.Refreshing
b2 = Sheet1.Range("ListObject2").ListObject.QueryTable.Refreshing
If b1 Or b2 Then
Call Application.OnTime(Now + PulseTimer, "NotifyWhenRefreshComplete")
Else
Call MsgBox("Refresh Complete.", vbOKOnly)
End If
End Sub
ListObject1 and ListObject2 are published tables. Only published tables are necessary to check if refresh is complete. You don't have to check unpublished tables.
However If you have a lot of published tables, then there is no wrong to iterate through all ActiveWorkbook.connections
and check if each and every wbConn.OLEDBConnection.Refreshing
status has return to false
state, replacing the b1
and b2
boolean.
Note: For some reasons, I refuse to use the DoEvents
, I want my users to be able to continue working with Excel while the connection is still running, and prompt them a message when the refresh is complete. But go ahead if you want to implement it with the Do... Loop
iteration instead of the OnTime
caller as shown above.
Lastly, there is a callback under Public WithEvents qt As QueryTable
, look for qt.refreshing
. You can use this method as well.
Hope this helps.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With