This question already has answers here:
Excel VBA - QueryTable AfterRefresh function not being called after Refresh completes
(2 answers)
Closed 5 years ago.
I am writing/maintaining an Excel VBA application where there are multiple QueryTables linked to MS sql server databases. Users of the application can alter the SQL query to each table by manipulating various UI controls on the Excel document.
One of the issues I have come across with QueryTables is there use of multi threading. Each QueryTable on the document has an original state that must be restored after a query is ran. For instance, if QueryTable1 had a base query of
Select * from example_table
and the user selected certain inputs on the controls to create
Select * from example_table Where object_oid = '10'
I would need the original state to be restored. The code below is a snapshot of how I am currently accomplishing this
Sub RefreshDataQuery()
'Dependencies: Microsoft Scripting Runtime (Tools->References) for Dictionary (HashTable) object
Dim querySheet As Worksheet
Dim interface As Worksheet
Set querySheet = Worksheets("QTable")
Set interface = Worksheets("Interface")
Dim sh As Worksheet
Dim qt As QueryTable
Dim qtDict As New Scripting.Dictionary
Set qtDict = UtilFunctions.CollectAllQueryTablesToDict
Set qt = qtDict.Item("Query from fred2")
''' Building SQL Query String '''
Dim sqlQueryString As String
Dim originalQueryCache As String
originalQueryCache = qt.CommandText
sqlQueryString = qt.CommandText
QueryBuilder.BuildSQLQueryStringFromInterface interface, sqlQueryString
MsgBox sqlQueryString
qt.CommandText = sqlQueryString
If Not qt Is Nothing Then
qt.Refresh
Else
'Error Messages and handling here
' Cut out to keep code short
End If
''' CLEAN UP '''
'Restore the original base SQL query
' Problem is here
' This, or any other altering statement, will error out if the query is still refreshing
qt.CommandText = originalQueryCache
' Other original state restoring code below...
' Free the dictionary
Set qtDict = Nothing
End Sub
Ideally, if I was writing this in another modern language, I would create a callback function or run the refresh in my own thread with a completion notifier. I spent a good chunk of time researching how to get a callback function for the qt.Refresh call, but am having no luck. I realize I could 'hack' around this a bit but I would prefer to not engage in bad practices as many people will have to maintain this in the future.
This application must support Excel 2010 versions and upward
So how can I create a callback function for VBA functions that are run in separate threads? Or, should I be looking at another approach?
The QueryTables events aren't exposed except through a custom class module and the WithEvents keyword. First, create a custom class module named CQtEvents and put this in it:
Private WithEvents mQryTble As QueryTable
Private msOldSql As String
Public Property Set QryTble(ByVal QryTble As QueryTable): Set mQryTble = QryTble: End Property
Public Property Get QryTble() As QueryTable: Set QryTble = mQryTble: End Property
Public Property Let OldSql(ByVal sOldSql As String): msOldSql = sOldSql: End Property
Public Property Get OldSql() As String: OldSql = msOldSql: End Property
Private Sub mQryTble_AfterRefresh(ByVal Success As Boolean)
Me.QryTble.CommandText = Me.OldSql
End Sub
That's two properties: one to hold the QueryTable and one to store the old sql. Then your procedure would look something like
Sub RefreshDataQuery()
Dim interface As Worksheet
Dim qt As QueryTable
Dim qtDict As New Scripting.Dictionary
Dim clsQtEvents As CQtEvents
Dim sqlQueryString As String
Set qtDict = UtilFunctions.CollectAllQueryTablesToDict
Set qt = qtDict.Item("Query from fred2")
sqlQueryString = qt.CommandText
QueryBuilder.BuildSQLQueryStringFromInterface interface, sqlQueryString
'Create class for events and store old sql
Set clsQtEvents = New CQtEvents
Set clsQtEvents.QryTble = qt
clsQtEvents.OldSql = qt.CommandText
qt.CommandText = sqlQueryString
If Not qt Is Nothing Then
qt.Refresh 'after this is done, the event in the class will fire
Else
'Error Messages and handling here
End If
End Sub
Because you define mQryTble with WithEvents, its two events (BeforeRefresh and AfterRefresh) are exposed in the class. By setting CQtEvents.QryTble to your QueryTable, the class then listens for events on that QueryTable. The CommandText is stored in OldSql before it's changed. Then when the Refresh is done, the event fires and the CommandText is restored. Of course not Refresh is done in the event, but I assume you want the old sql statement in there if it's refreshed or reprocessed.
Next, you should consider making a collection class to hold a bunch of QtEvents instances. I presume your code processes one as an example, but you're really doing more. Then you can move your CollectAllQueryTables inside that collection class and move the BuildSQL part inside the CQtEvents class.
Related
I wrote VBA code in Outlook to use AdvancedSearch. It worked.
When I moved it to Excel to be part of a larger routine, the event handlers stopped working.
The main code looks something like this.
Public gblnProcessAttachmentsDone As Boolean
Public gblnProcessAttachmentsStopped As Boolean
Sub ProcessAttachmentsSub()
' this routine performs the advanced search on a folder
...
gblnProcessAttachmentsDone = False
gblnProcessAttachmentsStopped = False
...
'perform search
Set objSearch = objOL.AdvancedSearch(strScope, strFilter, True, "ProcessAttachments")
Do Until gblnProcessAttachmentsDone
DoEvents
Loop
These are the event handlers.
Private Sub Application_AdvancedSearchComplete(ByVal SearchObject As Outlook.Search)
' this routine identifies the search that has just completed
If SearchObject.Tag = "ProcessAttachments" Then
Debug.Print "Search completed at " & Time
gblnProcessAttachmentsDone = True
End If
End Sub
Private Sub Application_AdvancedSearchStopped(ByVal SearchObject As Outlook.Search)
' this routine identifies the search that has just been stopped by the user
If SearchObject.Tag = "ProcessAttachments" Then
Debug.Print "Search stopped at " & Time
gblnProcessAttachmentsStopped = True
gblnProcessAttachmentsDone = True
End If
End Sub
I tried placing them in 'ThisWorkbook' and a Class module, but in both cases the events never get caught.
In Excel VBA, Application intrinsic variable points to Excel.Application, not Outlook.Application. Your event handler (Application_AdvancedSearchStopped) will not be automatically hooked up. Declare objOL with events and set up the event handler,.
To start an Outlook Automation session, you can use either early or late binding. Late binding uses either the Visual Basic GetObject function or the CreateObject function to initialize Outlook. For example, the following code sets an object variable to the Outlook Application object, which is the highest-level object in the Outlook object model. All Automation code must first define an Outlook Application object to be able to access any other Outlook objects.
Dim objOL as Object
Set objOL = CreateObject("Outlook.Application")
To use early binding, you first need to set a reference to the Outlook object library. Use the Reference command on the Visual Basic for Applications (VBA) Tools menu to set a reference to Microsoft Outlook xx.x Object Library, where xx.x represents the version of Outlook that you are working with. You can then use the following syntax to start an Outlook session.
Dim objOL as Outlook.Application
Set objOL = New Outlook.Application
To handle Outlook Application-level events in external applications:
First, you must declare a variable using the WithEvents keyword to identify the object whose event you want to handle.
Dim WithEvents objOL as Outlook.Application
Set objOL = New Outlook.Application
You can then select an Outlook application instance object in the Objects list of the module window and then select the event in the procedure list. The Visual Basic Editor will then add the template for the event procedure to the module window. You can then type the code you want to run when the event occurs.
Private Sub objOL_AdvancedSearchComplete(ByVal SearchObject As Outlook.Search)
' this routine identifies the search that has just completed
If SearchObject.Tag = "ProcessAttachments" Then
Debug.Print "Search completed at " & Time
gblnProcessAttachmentsDone = True
End If
End Sub
Read more about the AdvancedSearch method in the Advanced search in Outlook programmatically: C#, VB.NET article.
Excel now has the possibility to store personal filtering views to help collaboration in simultaniously used documents.
I could only find Microsoft documentation for an add-in, but the function is available in my Excel version of MS Excel for Microsoft 365 MSO (16.0.13127.20266) 32bit.
https://learn.microsoft.com/en-us/javascript/api/excel/excel.namedsheetview?view=excel-js-preview
I am trying to store the currently applied NamedSheetView name property (for later restoring option) but
this code fails:
Dim sh1 As Worksheet
Dim xViewName As String
Set sh1 = ThisWorkbook.Sheets(Sheet6.Name)
xViewName = sh1.NamedSheetView.Name
However this code works (with previously created "Test" view):
sh1.NamedSheetViews.GetItem("Test").Activate
If this NamedSheetViews is a collection, I should be able to get the item property, but these codes also fail:
strName = sh1.NamedSheetViews.GetItem(1).Name
strName = sh1.NamedSheetViews.Item(1).Name
Anyone has ever succeeded in getting the current NamedSheetView of a Worksheet?
Here is how I probe unknown Object properties:
I start with a reference to the Object. If I don't know what the Object is I use TypeName() to return it's class name (data type). I then declare a variable of that data type. Wash, rinse and repeat as I drill down the structure. Once the variable is declared, selecting the variable and pressing F1 with open the Microsoft Help document for that data type.
Module Code
Sub WhatIsThat()
Const TestName As String = "TestName"
Dim View As NamedSheetViewCollection
Set View = Sheet6.NamedSheetViews
On Error Resume Next
View.GetItem(TestName).Delete
On Error GoTo 0
View.Add TestName
Dim SheetView As NamedSheetView
Dim n As Long
For n = 0 To View.Count - 1
Debug.Print View.GetItemAt(n).Name
Set SheetView = View.GetItemAt(n)
Debug.Print SheetView.Name
Next
Stop
End Sub
Immediate Window Tests
?TypeName(Sheet6.NamedSheetViews)
?View.GetItemAt(0).Name
?TypeName( View.GetItemAt(0))
SOLUTION:
(Thanks for the great help from TinMan)
Dim SheetView As NamedSheetView
Dim sh1 As Worksheet
Dim ActiveSheetView as string
Set sh1 = ThisWorkbook.Sheets(Sheet6.Name)
Set SheetView = sh1.NamedSheetViews.GetActive
ActiveSheetView = SheetView.Name
Application:
sh1.NamedSheetViews.GetItem(ActiveSheetView).Activate
I am trying to run an Access Macro from Excel. I set a reference to Microsoft Office 14.0 Access database engine Object Library. Now, I'm trying to run a small script like this.
Sub RunAccessMacro()
Dim strDatabasePath As String
Dim PathToDB As String
Dim db As DAO.Database
Dim rs As DAO.Recordset
PathOfDatabase = ThisWorkbook.Worksheets("Updates").Range("PathToAccess")
Set db = DAO.DBEngine.OpenDatabase(PathOfDatabase)
Set qry = db.Execute("Macro_Run_Key")
qry.Close
db.Close
MsgBox "Done! All processes complete!!"
End Sub
The problem is, the db.Execute won't execute the Macro. I don't even see anything like RunMacro, or whatever it's called.
There must be a way to do this, right.
The database engine only does database things (anything with tables and queries). If you want more than that, you will have to use the Access application through VBA:
Sub RunAccessMacro()
Dim strDatabasePath As String
Dim PathToDB As String
Dim accApp As Access.Application
Set accApp = New Access.Application
PathOfDatabase = ThisWorkbook.Worksheets("Updates").Range("PathToAccess")
accApp.OpenCurrentDatabase PathOfDatabase
accApp.DoCmd.RunMacro "Macro_Run_Key"
accApp.Quit
Set accApp = Nothing
MsgBox "Done! All processes complete!!"
End Sub
Also, you will need to add a reference to the Microsoft Access Object Library, or you can adapt this code to use late bindings.
Setting a reference to the Access Object Library is useless unless you actually use it :) Seriously, the code written above uses DAO which is a different animal than calling Access directly through the Object Libary. DAO is strictly a database engine (like ADO) and does not know about macros and modules and such as defined in Office Apps.
Here is the code to use when using early binding:
Sub RunAccessMacro()
Dim PathOfDatabase As String
PathOfDatabase = ThisWorkbook.Worksheets("Updates").Range("PathToAccess")
Dim accApp As Access.Application
Set accApp = New Access.Application
With accApp
.OpenCurrentDatabase PathOfDatabase
.DoCmd.RunMacro "Macro_Run_Key"
.Quit
End With
MsgBox "Done! All processes complete!!"
End Sub
If the macro is an Access Macro, you can actually trigger the macro with a single command rather than having to go around the houses. The /X command line switch will help - check this link: How can I schedule a Macro to run automatically in Access 2007
We all know that ms access is not multitreaded so when msacces runs a long query it hangs waiting to the query to be completed. what i want is open from access an new instance of access to run a query or run vba code in background. after running it needs to kit itself after it turns back the results (maybe though the sql server background)
i have seen something before in excel but i wonder if it is posible to do in access
the excel variant is here [excel swarm][1
UPDate
i open access with the folowing code
Dim appAccess As Object
Set appAccess = CreateObject("Access.Application")
Call appAccess.OpenCurrentDatabase( _ "D:\test.accdb")
appAccess.UserControl = True
Set appAccess = Nothing
the target access db is preformatted with a loop as test with is started when access opens. the problem is that the souce access hangs during starting ans running of the target access.
i can use the timer to give it a delayed start and then its working.
the main problem is how can i stat a not preformated access db, create things like vba code, querys odbc connections etc and run it without the source db being hanging.
you could try something like this from a shell command to another VBA host, say excel, which could trigger the event. This is a class, where the properties of DB path and Query name are passed in, then GO is executed, it uses the Execute Complete event of the DBs ADO connection, I've coded it to create an Excel instance and populate with the results.
Ive not tested this fully as in the middle of something, but i'll test fully at lunch and edit as req'd, but a starting point
Option Explicit
Private WithEvents c As ADODB.Connection
Private strDBPath As String
Private strQueryToRun As String
Public Property Let DBPath(strPath As String)
strDBPath = strPath
End Property
Public Property Let QueryToRun(strQuery As String)
strQueryToRun = strQuery
End Property
Public Function GO()
Dim a As New Access.Application
a.OpenCurrentDatabase strDBPath, False
Set c = a.CurrentProject.Connection
c.EXECUTE strQueryToRun
a.CloseCurrentDatabase
a.Quit
Set a = Nothing
End Function
Private Sub c_ExecuteComplete(ByVal RecordsAffected As Long, ByVal pError As ADODB.Error, adStatus As ADODB.EventStatusEnum, _
ByVal pCommand As ADODB.Command, ByVal pRecordset As ADODB.Recordset, ByVal pConnection As ADODB.Connection)
' what to do with the results?
Dim xl As New Excel.Application
Dim xlWb As Excel.Workbook
xl.Visible = True
Set xlWb = xl.Workbooks.Add
xlWb.Sheets(1).Range("a1").CopyFromRecordset pRecordset
End Sub
Yes, that is possible.
Use command Shell to open another instance of Access - and add command line parameters Access to hold info about which queries to run.
Hi guys i am new here and i am new to vba.
i want to solve the following problem:
i have two different access tables. each of them contains data i want to compare first and then, if a certain constraint is true i want to import certain columns out of one of the two access db tables into an excel sheet.
what i already have: the connection to the databases, i can read the data and print them on the console via debug.print command.
i have really no idea how to write certain rows (those which conform to the constraint) to the excel sheet.
Code sample
'commandstring and data base variables stands here
'non database connection variables
Dim oldID, newID, oldBuildPlanned, newBuildPlanned As String
Dim createExcel, doesExcelExist As Boolean
Dim xl As Excel.Application
Dim wb As Excel.Workbook
Dim Wksht As Excel.Worksheet
Dim dataVar As String
Dim counter As Integer
counter = 0
createExcelSheet = False
doesSheetExist = False
'Debug.Print "TEST old database"
Do While Not objRs.EOF And Not objRs2.EOF
'Debug.Print vbTab & objRs(0) & " " & objRs(1)
'assigning database values to variables to make them comparable
oldID = objRs(counter)
newID = CStr(objRs2(counter))
oldBuildPlanned = objRs(counter + 1)
newBuildPlanned = objRs2(counter + 1)
If oldID = newID And oldBuildPlanned = newBuildPlanned Then
createExcel = True
If createExcelSheet = True And Not doesSheetExist = True Then
Set xl = New Excel.Application
Set wb = xl.Workbooks.Add
Set Wksht = ActiveWorkbook.Worksheets("Sheet1")
doesExcelExist = True
End If
Call writeReport(newID)
End If
objRs.MoveNext
objRs2.MoveNext
Loop
'tidy up stuff comes here
end of code
I am sorry if my code is not formatted as its usual, its my first post in this forum ^^
So the writeReport() should contain the code to write the data into the sheet. i planned to insert the id's of the matching database entries into the method as parameters and read these certain data out of the recordset. but i cannot convert recordset items to string, so the byRef parameter declaration causes a compile error "type mismatch". In addition i tried to export the table with DoCmd.TransferSpreadsheet, but this method exports the entire table into excel, it worked, but it is not what i am searching for.
i hope someone can help me with my little low level problem, if you need further information feel free to ask me. i am using ADO.
Thanks in advance
Welcome to the forum,
I think you might find these two websites helpful for what you are trying to do. The first one is a great tutorial on using Access and Excel together.
http://www.datawright.com.au/excel_resources/excel_access_and_ado.htm
http://www.w3schools.com/sql/default.asp
I am not sure how you are creating your recordset, by I would recommend using an SQL statement as your source. That way you only pull the data from Access that you need. If you have any more specific questions, please let me know.
Jason