Data connection refreshes but the data does not using VBA - excel

I have a data connection in a workbook and I refresh it using VBA.
sub RefreshData()
ActiveWorkbook.Connections("LoadData1").Refresh
End Sub
The code runs without error but the data does not change or update. But when I step through the code, it works fine and the data is updated. I also tried doing a wait.
sub RefreshData()
ActiveWorkbook.Connections("LoadData1").Refresh
Application.Wait (Now + TimeValue("00:00:20"))
End Sub
I have also tried ActiveSheet.Unprotect, Application.ScreenUpdating = True but to no avail.

Simple Solution
I had issues with some of my subs ending before the data had finished refreshing. If I understand correctly, that's what's happening with your sub. If so, you could try adding the line:
Application.CalculateUntilAsyncQueriesDone
Adding this line into my code directly after the refresh command worked for me.
Note: When I had the queries set as background queries, I occasionally got weird freeze/crash issues; so I would recommend turning off the background query option with any query you use the above code with.
Complex Solution
If the simple solution doesn't work, an alternative is to add a custom class that raises an event when the refresh is finished. The downside to this solution is that you may need to rewrite existing code to be triggered by an event, instead of having an in-line refresh command.
An example of such a custom class is below. Please note that there are some assumptions built into the code -- the most prominent being that the query is set to load onto a sheet in the workbook, and refresh in the background.
To use the custom class, insert a "class module" (this is not the same as a "Module"), and copy the code from the "class code" section below into the "class module". Next, in the code module for the worksheet holding the resulting query table, add this code:
Private WithEvents queryData As QueryClass
Public Sub querySetup()
Set queryData = New QueryClass
Set queryData.QryTble = Me.ListObjects("QueryName").QueryTable
End Sub
Private Sub queryData_Refreshed(ByVal RefreshSuccess As Boolean, ByVal isEmpty As Boolean)
End Sub
(Note that this code is assuming that the class module has been renamed to "QueryClass", and that the query was named "QueryName". If you used different names, you'll need to adjust the code accordingly.)
You can put custom code in the queryData_Refreshed sub to happen after the query has finished refreshing. Note that the sub has two indicators -- if the query refreshed successfully, and if the query is empty (did not return any records). Then, to refresh the data, just call:
queryData.Refresh 5 'optional maximum of attempts; defualt is 1
These questions may also be helpful.
Class Code
Option Explicit
'class basics from Paul Renton, https://stackoverflow.com/questions/18136069/excel-vba-querytable-afterrefresh-function-not-being-called-after-refresh-comp
Private WithEvents mQryTble As Excel.QueryTable
Private RefreshFinished As Boolean
Private RefreshSuccessful As Boolean
Private attemptCount As Long
Private attemptMax As Long
Public Event Refreshed(ByVal RefreshSuccess As Boolean, ByVal isEmpty As Boolean)
Public Property Set QryTble(ByVal QryTable As QueryTable)
Set mQryTble = QryTable
End Property
Public Property Get QryTble() As QueryTable
Set QryTble = mQryTble
End Property
Public Property Get RefreshDone() As Boolean
RefreshDone = RefreshFinished
End Property
Public Property Get RefreshSuccess() As Boolean
RefreshSuccess = RefreshSuccessful
End Property
Private Sub mQryTble_AfterRefresh(ByVal Success As Boolean)
attemptCount = attemptCount + 1
If Success Or attemptCount = attemptMax Then
RefreshFinished = True
RefreshSuccessful = Success
RaiseEvent Refreshed(Success, mQryTble.ListObject.DataBodyRange Is Nothing)
Else
mQryTble.ListObject.Refresh
End If
End Sub
Public Sub Refresh(Optional attempts As Long = 1)
If Not mQryTble.Refreshing Then
RefreshFinished = False
attemptMax = attempts
mQryTble.ListObject.Refresh
End If
End Sub

Related

When assigning value to class variable in VBA excel, the value is not set. How do I set a class declared variable?

I'm pretty new to VBA but I have been trying to figure out what is going wrong here. When I try to set a variable declared in my class module from a Sub in my module, the value isn't assigned for some reason and I can't figure out why. How do I get the variable table to go all the way through to the function call response = add_edit_data("fund_management.db", table)? It all compiles and runs, until it reaches the add_edit_data function which of course does not work without this variable. My Debug.Print checkpopup.table in the Sub returns nothing although I know the variable table from calling the Sub is set correctly as Debug.Print table returns the correct value when called from inside the Sub.
My module looks like this:
Private checkpopup As class_checkpopup
Public Sub checkbox_popup_fund(table)
Set checkpopup = New class_checkpopup
checkpopup.Show
checkpopup.table = table
Debug.Print checkpopup.table
End Sub
The class module is:
Public table As String
Private WithEvents check_box_popup As check_box
Private Sub Class_Initialize()
Set check_box_popup = New check_box
End Sub
Public Sub Show()
check_box_popup.Show
End Sub
Private Sub check_box_popup_Closed()
End Sub
Private Sub check_box_popup_Yes()
response = add_edit_data("fund_management.db", table)
End Sub
and I activate the eventhandler with:
Public Event Yes()
Private Sub check_box_yes_Click()
RaiseEvent Yes
End Sub
Thank you to Nicholas Hunter and Variatus for helping me out. I wasn't aware that it mattered to set the variable before showing the Form, but it does. I changed my module to:
Private checkpopup As class_checkpopup
Public Sub checkbox_popup_fund(table)
Set checkpopup = New class_checkpopup
Let checkpopup.table = table
checkpopup.Show
Debug.Print checkpopup.table
End Sub
and now my value carries through all the way to my function. Thank you very much for the help

How to log the time an external data connection/query refreshes?

I have an external data query in Excel connected to a Microsoft Access database that I refresh once a day to populate a table in Excel, and I want to measure exactly how long it takes to refresh the query/connection.
I believe I am close with a solution for this using VBA on the worksheet and the TableUpdate selection there? But can't seem to get anything to actually work with this.
FYI, I am doing this as with some recent Excel updates, a couple of my queries have started to take double the time, and so I am wanting to revert back to old Excels and then measure how long it takes to refresh, and as it takes up to 15 minutes, I can't be sitting there watching it, and record the time, but I will click refresh, do something else for 20 mins, and find out when it's finished refreshing.
You can create a "custom" timed query table using a class and event sinking, like so.
A class called clsTimedQueryTable, with the following code
Option Explicit
Private WithEvents qtTimed As QueryTable
Private tmStart As Date
Private tmEnd As Date
Public Property Get RefreshTimeTaken() As Variant
RefreshTimeTaken = DateDiff("m", tmStart, tmEnd)
Debug.Print RefreshTimeTaken
End Property
Public Sub INIT(qtToTime As QueryTable)
Set qtTimed = qtToTime
End Sub
Private Sub qtTimed_AfterRefresh(ByVal Success As Boolean)
tmEnd = Now
Module1.TimeTaken = RefreshTimeTaken
Debug.Print "Ended : " & tmEnd
End Sub
Private Sub qtTimed_BeforeRefresh(Cancel As Boolean)
tmStart = Now
Debug.Print "Started : " & tmStart
End Sub
Then in a normal module have the following
Private clsQueryTable As clsTimedQueryTable
Public TimeTaken As Double
Private Sub SetUp()
Set clsQueryTable = New clsTimedQueryTable
clsQueryTable.INIT ActiveSheet.ListObjects(1).QueryTable
End Sub
Refreshing the table will now handle the timings and pass the result back to TimeTaken. There is no need to do the way I have to put the variable in, it's from the class property also.
I would also add a function .RefreshTable in the class too, to control it all from the class, rather than set class, right click refresh, etc.
Up to you.

How can I run a userform from a macro?

I developed many UDFs and macros in VBA for Excel 2016. One of my macros uses an Inputbox to get data used subsequently by the macro. I want to replace the Inputbox with a user form. I have created the user form, with one text box. I want to activate the user form, populating the text box with the default data, and return the text box data to the macro when OK is selected. I have searched extensively for an end-to-end example for all the the code needed to do this, with no luck. Does an example for this simple problem exist?
Add a Property to your user form. For this answer, let us use the following code within the user form.
Public Property Get MyResult() As String
' You may want to do any manipulation here
' including converting to a number, in which case the return type should be changed (*)
MyResult = TextBox1.Text
End Property
(*) If you are doing conversion, you can have another function in your user form to disable the "OK" button until they have valid convertible data in the text box.
You also want to know if they have hit "Cancel"
Public Property Get Cancelled() As Boolean
Cancelled = pCancelled ' Declare pCancelled as a Boolean in the scope of the form
End Property
Public Sub CancelButton_Click() ' Standard click event for the button
pCancelled = True
Me.Hide
End Sub
Public Sub OKButton_Click() ' Standard click event for the button
pCancelled = False
Me.Hide
End Sub
In your calling macro
MyForm.Show ' This is modal, so will block execution until a response is provided
If Not MyForm.Cancelled Then
Debug.Print MyForm.MyResult
'Do something with MyForm.MyResult
End If
UnLoad MyForm ' assuming you do not want to re-use this form as part of your logic.
There is an example of how you can pass the value to a form and get the result back. The approach uses Scripting.Dictionary object created within standard module scope and passed to userform to allow values to be changed. So it makes possible to send the default values to userform, and keep the result values in the dictionary even after the userform is closed and unloaded. You may have multiple values, just add the necessary quantity of keys to the dictionary, e. g. oData("property1"), oData("property2"), etc.
Add a standard module to the project and put the below code into it:
Option Explicit
Sub Test()
Dim oData
' Set default value and show form
Set oData = CreateObject("Scripting.Dictionary")
oData("") = "Some default text"
UserForm1.ShowForm oData
' Wait until user close form
Do While IsUserFormLoaded("UserForm1")
DoEvents
Loop
' Output returned value
MsgBox oData("")
End Sub
Function IsUserFormLoaded(UserFormName As String) As Boolean
Dim oUF As Object
For Each oUF In UserForms
If LCase(oUF.Name) = LCase(UserFormName) Then
IsUserFormLoaded = True
Exit Function
End If
Next
End Function
Add a userform module named UserForm1 to the project, place controls as shown:
And put the below code into the userform module :
Private opData
Public Sub ShowForm(oData)
Set opData = oData
Me.TextBox1.Value = opData("")
Me.Show
End Sub
Private Sub UserForm_Initialize()
If TypeName(opData) <> "Dictionary" Then Set opData = CreateObject("Scripting.Dictionary")
End Sub
Private Sub CommandButton1_Click()
Unload Me
End Sub
Private Sub CommandButton2_Click()
opData("") = Me.TextBox1.Value
Unload Me
End Sub

"Include" in Excel VBA?

I have a user-form which is made up of many subs, this is assigned as a macro to a button on the worksheet. When the user is finished with this user-form they can press a button on it which causes its visibility to become false and when entered again everything appears how it was left resulting in a save like feature.
I now need to apply this to multiple buttons on the worksheet and each user form needs to have the exact same code and same buttons but be a separate form as each individual button requires it's own save like feature. The way I was planning on doing this was to copy the existing user form and paste it many times with different names however, if a modification is required it will take a long time to carry out therefore, is there a method such as "include" which could use a base module from which all the code is accessed so that if I ever need to change anything I just do it on that one module and everything else updates via the include?
EDIT:
I now have a public function called costing() and am getting an error when I used:
Private Sub material_Change()
Call costing
End Sub
You can have multiple instances of the same form. You can use this to retain multiple sets of form values
Try this:
Create your form, as usual. Let's call it MyForm
Create several buttons on your sheet. My example uses ActiveX buttons, but Form Control buttons can be used too. Let's call them CommandButton1 and CommandButton2
In your form module, include a Terminate Sub, which includes this code
Private Sub UserForm_Terminate()
' any other code you may need...
Unload Me
End Sub
The Form buton to save/Hide the form needs to be
Private Sub btnSaveAndHide_Click()
Me.Hide
End Sub
The Sheet Button code is as follows
The code is identical for each button (and calls a common Sub), and each button has its own Static form variable.)
The Error handler is needed to deal with the case a form is not properly closed. In this case the instance no longer exists, but the local Static variable is also not Nothing
Example shows form shown as Modeless, you can change this to Modal if you want.
Private Sub CommandButton1_Click()
Static frm As MyForm
ShowMyForm frm
End Sub
Private Sub CommandButton2_Click()
Static frm As MyForm
ShowMyForm frm
End Sub
Private Sub ShowMyForm(frm As MyForm)
If frm Is Nothing Then Set frm = New MyForm
On Error GoTo EH
frm.Show vbModeless
Exit Sub
EH:
If Err.Number = -2147418105 Then
On Error GoTo 0
Set frm = Nothing
Set frm = New MyForm
frm.Show
End If
On Error GoTo 0
End Sub
End result: multiple copies of the same form, each with their own values
In responce to comment How would I access the variables inside of each user form externally
In the example above the Form instances are only accessable in the Command Button Click Handler routines, or within the Form module itself. If you can write your code in the form module, then no change is needed.
To make the Form instances available elsewhere, consider moving their declaration to Module Scope of a standard Module. You could declare them as, eg individual variables, an array (either static or dynamic), a Collection, a Dictionary. Which structure is best will depend on how you want to manage and access your form instances.
For example, a Static Array: Code in a standard Module
Option Explicit
Global MyForms(1 To 2) As MyForm
Update the CommandButton code to
Private Sub CommandButton1_Click()
ShowMyForm Module1.MyForms(1)
End Sub
Private Sub CommandButton2_Click()
ShowMyForm Module1.MyForms(2)
End Sub
Private Sub ShowMyForm(frm As MyForm) no change, same as before
The code works the same as before, but you can now access the Global variable in a standard Module
Sub Demo()
Dim i As Long
For i = LBound(MyForms) To UBound(MyForms)
If Not MyForms(i) Is Nothing Then
MsgBox "Form " & i & " Value = " & MyForms(i).TextBox1.Value
End If
Next
End Sub
You don't need an "Include" (none exists in VBA); all you need to do is create a module and make the common methods public.
For example, if you create a module and have a function like this:
Public Function Add(first As Integer, second As Integer) As Integer
Add = first + second
End Function
Then you can access it like this from another module/form/class module:
Sub test()
MsgBox Add(3, 6)
End Sub

Get Selected value of a Combobox

I have a thousands of cells in an Excel worksheet which are ComboBoxes. The user will select one at random and populate it.
How do I get the selected ComboBox value? Is there a way to trigger a function (i.e. an event handler) when the ComboxBoxes has been selected?
You can use the below change event to which will trigger when the combobox value will change.
Private Sub ComboBox1_Change()
'your code here
End Sub
Also you can get the selected value using below
ComboBox1.Value
If you're dealing with Data Validation lists, you can use the Worksheet_Change event. Right click on the sheet with the data validation and choose View Code. Then type in this:
Private Sub Worksheet_Change(ByVal Target As Range)
MsgBox Target.Value
End Sub
If you're dealing with ActiveX comboboxes, it's a little more complicated. You need to create a custom class module to hook up the events. First, create a class module named CComboEvent and put this code in it.
Public WithEvents Cbx As MSForms.ComboBox
Private Sub Cbx_Change()
MsgBox Cbx.Value
End Sub
Next, create another class module named CComboEvents. This will hold all of our CComboEvent instances and keep them in scope. Put this code in CComboEvents.
Private mcolComboEvents As Collection
Private Sub Class_Initialize()
Set mcolComboEvents = New Collection
End Sub
Private Sub Class_Terminate()
Set mcolComboEvents = Nothing
End Sub
Public Sub Add(clsComboEvent As CComboEvent)
mcolComboEvents.Add clsComboEvent, clsComboEvent.Cbx.Name
End Sub
Finally, create a standard module (not a class module). You'll need code to put all of your comboboxes into the class modules. You might put this in an Auto_Open procedure so it happens whenever the workbook is opened, but that's up to you.
You'll need a Public variable to hold an instance of CComboEvents. Making it Public will kepp it, and all of its children, in scope. You need them in scope so that the events are triggered. In the procedure, loop through all of the comboboxes, creating a new CComboEvent instance for each one, and adding that to CComboEvents.
Public gclsComboEvents As CComboEvents
Public Sub AddCombox()
Dim oleo As OLEObject
Dim clsComboEvent As CComboEvent
Set gclsComboEvents = New CComboEvents
For Each oleo In Sheet1.OLEObjects
If TypeName(oleo.Object) = "ComboBox" Then
Set clsComboEvent = New CComboEvent
Set clsComboEvent.Cbx = oleo.Object
gclsComboEvents.Add clsComboEvent
End If
Next oleo
End Sub
Now, whenever a combobox is changed, the event will fire and, in this example, a message box will show.
You can see an example at https://www.dropbox.com/s/sfj4kyzolfy03qe/ComboboxEvents.xlsm
A simpler way to get the selected value from a ComboBox control is:
Private Sub myComboBox_Change()
msgbox "You selected: " + myComboBox.SelText
End Sub
Maybe you'll be able to set the event handlers programmatically, using something like (pseudocode)
sub myhandler(eventsource)
process(eventsource.value)
end sub
for each cell
cell.setEventHandler(myHandler)
But i dont know the syntax for achieving this in VB/VBA, or if is even possible.

Resources