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

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.

Related

Userform initialize without rewriting data in the cell

I have been wondering about this one for a while now.
Let's say I have a formula in A1, Worksheet("Main")
=IF(B2="English";"Good morning";"Guten Morgan")
Then I have userform with code:
Private Sub TextBox1_Change()
ThisWorkbook.Worksheets("Main").Range("A1").Value = Me.TextBox1.Text
End Sub
Private Sub UserForm_Initialize()
Me.TextBox1.Text = ThisWorkbook.Worksheets("Main").Range("A1").Value
End Sub
How can I make it work so, that if I don't input anything into textbox, it will keep displaying functions result. If I will start to type text into textbox it will input my typed text to A1. Now if I open the userform it will overwrite A1 with the text in textbox and there will be no formula anymore. So if I change language in B2 result will no longer be interfaced into textbox.
Can be also some other approach with VBA. Everything is acceptable as long as logic will work.
I have tried to use textbox properly, something like linkedsource or similar, but it is crashing excel workbook sometimes. That's why I am trying to avoid it.
EDIT:
Thank you for suggestions! I have tried to implement this somehow but still don't get it. I am creating variable where I want to store result from ThisWorkbook.Worksheets("Other Data").Range("L49").Value then I would like to use it in Userform Me.TextBox14.Text to be displayed. Then once it is changed in Me.TextBox14.Text and Enter button has been pressed it should change also in ThisWorkbook.Worksheets("Other Data").Range("L49").Value.
Here is my current code I am trying to play with:
Private ProjectClass As String
Private Sub TextBox14_Enter()
ThisWorkbook.Worksheets("Other Data").Range("L49").Value = ProjectClass
End Sub
Private Sub UserForm_Initialize()
Me.TextBox14.Text = ProjectClass
End Sub
The TextBox.Enter event isn't fired when the user presses Enter, but when the control is entered - that is, when it gets the focus and a caret/cursor starts blinking inside it. You'll want to update the backing variable when the value is modified:
Private Sub TextBox14_Enter()
'runs when the control gets focus
End Sub
Private Sub TextBox14_Exit()
'runs when the control loses focus
End Sub
Private Sub TextBox14_Change()
'runs whenever the value changes (real-time)
End Sub
So in this case I'd go with the TextBox.Change event handler, and make it update the variable (not the worksheet):
Private ProjectClass As String
Private Sub TextBox14_Change()
ProjectClass = TextBox14.Text
End Sub
Now the problem is that the ProjectClass value needs to be accessible from outside the form, so that the caller can set an initial value. One way to do this could be to expose it as a property - one property (get+let) for each field you want to seed a value for:
Public Property Get ProjClass() As String
ProjClass = ProjectClass
End Property
Public Property Let ProjClass(ByVal value As String)
ProjectClass = value
ApplyModelProperties
End Property
Private Sub ApplyModelProperties()
TextBox14.Text = ProjectClass
'...
End Sub
Now from outside the form, at the call site (the code that's showing this dialog), you can seed the value from the worksheet, and the form never needs to know or care that a worksheet was involved:
With New UserForm1
.ProjClass = ThisWorkbook.Worksheets("Other Data").Range("L49").Value
.Show
MsgBox .ProjClass
End With
Note that because the value is exposed as a property, the calling code doesn't need to know about TextBox14 anymore.

Data connection refreshes but the data does not using VBA

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

"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.

Change displayed sheet after user inactivity

I am trying to create a piece of code that links to a certain sheet after a certain amount of time of inactivity, what I mean by inactivity is not switching through sheets, so when somebody is clicking on sheets that counts as activity but as soon as its been on the same sheet for an amount of time I want it to switch to sheet 1 (sheet 1 is linked to a presentation and will act much like a screensaver would)
Here is my code in ThisWorkbook
Private nTime As Date
Const proc As String = "SelectIndex"
Private Sub Workbook_SheetChange(ByVal Sh As Object, ByVal Source As Range)
Call SetTimer
End Sub
Private Sub SetTimer()
If nTime <> 0 Then
Call Application.OnTime(EarliestTime:=nTime, Procedure:=proc, Schedule:=False)
End If
nTime = Now + TimeValue("00:00:05")
Application.OnTime nTime, Procedure:=proc
End Sub
This works for entering data, when somebody doesn’t enter data for so long it goes to my sheet, but I want it to do it if somebody isn’t switching sheets because nobody has access to enter data anyway, just view the sheets.
It also only works once, when you cancel the presentation and try it again I get the error
"Run time error '1004' Method 'OnTime' of object '_ Application'
Failed "
Just these two problems to overcome and I would really appreciate it if anyone could help J
For information, the procedure SelectIndex is just a macro that switches to sheet 1
If I understand the question correctly, you could trigger the timer within the Sheet_Activate event:
Private Sub Workbook_SheetActivate(ByVal Sh As Object)
Call SetTimer
End Sub

Resources