I'm attempting to create a checkbox that will pause the current function when clicked. Here's how my code is set up-
Public Function Wait()
LogOutput("Application paused.")
Do Until waitBox.Checked = False
'Task.Delay(1000)
My.Application.DoEvents()
Loop
LogOutput("Application unpaused.")
Return Nothing
End Function
Public Function Thing()
For Each i In x
If waitBox.Checked Then
Wait()
Else
DoStuff()
End If
Loop
Return Nothing
End Function
As you can see I've tried "Task.Delay" as well as "DoEvents(), however the problem is that the GUI is locked up while the function is running and so the user is unable to click pause at all.
How can I get around this?
Thanks in advance.
You need to create a new thread for that, so the GUI is not locked:
https://msdn.microsoft.com/en-us/library/system.threading.thread(v=vs.110).aspx
Related
I am writing a library to automate internet explorer. The library sets up its own message loop by starting a thread and using Dispatcher.Run. The reason for this is to keep internet explorer and mshtml on the same thread.
I then use the following in the library.
Private ExDispatcher As Dispatcher
Private Success As Boolean
Private EXThreadWaiter As Threading.AutoResetEvent
Public Async Function ClickAndWaitForNewPageAsync() As Task(Of Boolean)
Return Await Task.Run(Of Boolean)(Function()
ExDispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, Sub() startClickAndWaitForNewPage())
EXThreadWaiter.WaitOne()
Debug.Print("Completed with: " + Success.ToString)
Return Success
End Function)
End Function
Private Sub startClickAndWaitForNewPage()
'do a lot of stuff and wait for internet explorer
'if we are happy and all good set Success = True
'now let the thread in the task go
EXThreadWaiter.Set()
End Sub
ClickAndWaitForNewPageAsync starts a task which is on a new thread, it immediately then uses my message loop to call startClickAndWaitForNewPage and all the Internet Explorer stuff is done. When everything is ok the variable Success is set to a value and EXThreadWaiter.Set() is called which releases the task thread and the result is returned.
A client of this library therefore uses it as shown below.
Private Async Sub someFunction()
Dim res As Boolean = Await ClickAndWaitForNewPageAsync()
If res Then
'continue on with code
Else
'do nothing
End If
End Sub
The issue I have with this is how to shutdown or stop my library which needs to happen for various reasons. I currently have this method in the same class as ClickAndWaitForNewPageAsync
Public Sub StopIt()
Success = False
EXThreadWaiter.Set()
End Sub
This method sets Success to False and lets the task continue. This mostly works fine but it is possible that the following happens.
startClickAndWaitForNewPage sets Success to True
EXThreadWaiter.Set() is called
Debug.Print("Completed with: " + Success.ToString) indicates Success is True
StopIt() runs setting Success = False
someFunction in the client code receives Success = True
This means that in someFunction it continues with its code thinking everything is ok but in fact it is not.
How can I prevent this from happening?
By the way the Cancellation Token etc i do not think will help, but I'm happy to hear differently.
What I previously used
I previously did not use Tasks. I would simply use the Dispatcher begininvoke which would mean everything is done on the right thread and the client would have to subscribe to an event that would fire when everything was done successfully. This felt untidy to me and thus I am trying this new method.
I have written in vb.net for progress bar. I am thinking of there is a better way than this.
Here is what my code is :
Private Function ImportDataFiles(ByRef pobjDataLoadDTO As DataLoadDTO) As Boolean
Try
lblStatus.Visible = True
lblStatus.Text = ""
myProgressBar.Visible = True
myProgressBar.Value = 0
For Each drRow As ImportData.TRow In pobjDataLoadDTO.FileInfo.Select("categ_code = 'abc'")
If pobjDataLoadDTO.FileTimes.ContainsKey(drRow.KEY_CODE) AndAlso _
pobjDataLoadDTO.FileTimes(drRow.KEY_CODE) > pobjDataLoadDTO.UploadTimes(drRow.KEY_CODE) Then
pobjDataLoadDTO.DestinationTablename = drRow.KEY_CODE
If mobjDataLoadBO.ImportDataFiles(pobjDataLoadDTO) Then
drRow.DATA_TXT = mobjCommonBO.ONow.ToString
End If
End If
lblStatus.Text = drRow.KEY_CODE.Trim & "is loading...."
lblStatus.Refresh()
myProgressBar.PerformStep()
lblStatus.Refresh()
Next
Return True
Catch ex As Exception
Return False
End Try
End Function
Right now It is working, But I want to use more efficient way, like using Backgroundworkerprocess...etc., Any Ideas on this one?
Since your function runs in main thread, I assume your application is freezing and not very smooth while upload in progress.
1 - Drop Backgroundworker control on the form
2 - set "reportProgress" property of the worker to "True"
3 - Move your loop code into "DoWork" event of the worker control. And call worker.RunWorkerAsync. You can pass needed arguments to it
4- the code that refreshes progress bar move into "ProgressChange" event of the worker. This is important as you can't call control from worker thread. and ProgressChange is running in the main thread. You can also delete "Refresh" method call. That will not be needed anymore. Every time you want to refresh the progress bar call "ReportProgress" method of the worker
5-Use "RunWorkerCompleted" worker event, to do your clean up, and hide your progress bar
Its also might be a good idea to check if worker is already working before initiating, like
If worker.IsBusy Then
If worker.CancellationPending = False Then
worker.CancelAsync()
End If
Exit Sub
End If
Is there some way to have a 'listener' of sorts to listen for a button click in the middle of code? There are certain scenarios where I won't have to wait for the code to complete before I can exit the functions but I cannot seem to find a way to see if the button was clicked other than throwing a ton of if checks throughout the function calls which doesn't seem very efficient to me.
This code runs on the thread, different from your main application
private function LongRunningFunction()
Din canceled as boolean = false ' to make sure it is canceled while in the loop
' this is very long loop
For i as integer = 0 to 100000
If _cancelExecution Then
canceled = true
Exit For
End If
' your code runs here
.........
Next
If canceled Then
' Wrap up this thread, clean up stuff, etc
End If
.......................
End Function
On the main thread, when button is clicked _cancelExecution is set to true
The BackGroundWorker has this mechanism built-in already
I am developing an Excel (2010+) Application using VBA and have run into an issue where the AfterRefresh event function is not being invoked once the query finishes executing.
I have not been able to find many decent resources or documentation for how to have this event function triggered in a Class Module. I decided to use the Class Module design route instead of putting the event handlers in the worksheet after receiving a response to an earlier question about QueryTables (found here Excel VBA AfterRefresh).
Here is the code for my Class Module called CQtEvents
Option Explicit
Private WithEvents mQryTble As Excel.QueryTable
Private msOldSql As String
' Properties
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 Let OldSql(ByVal sOldSql As String): msOldSql = sOldSql:
End Property
Public Property Get OldSql() As String: OldSql = msOldSql:
End Property
Private Sub Class_Initialize()
MsgBox "CQtEvents init"
End Sub
' Resets the query sql to the original unmodified sql statement
' This method is invoked when the Refresh thread finishes executing
Private Sub mQryTble_AfterRefresh(ByVal Success As Boolean)
' Problem is here
' This function is never called :( Even if the query successfully runs
Me.QryTble.CommandText = Me.OldSql
End Sub
Here is a quick snapshot of the code the creates an instance of this class, finds a relevant QueryTable, then calls Refresh
Option Explicit
Sub RefreshDataQuery()
'Dependencies: Microsoft Scripting Runtime (Tools->References) for Dictionary (HashTable) object
'From MGLOBALS
cacheSheetName = "Cache"
Set cacheSheet = Worksheets(cacheSheetName)
Dim querySheet As Worksheet
Dim interface As Worksheet
Dim classQtEvents As CQtEvents
Set querySheet = Worksheets("QTable")
Set interface = Worksheets("Interface")
Set classQtEvents = New CQtEvents
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
sqlQueryString = qt.CommandText
Set classQtEvents.QryTble = qt
classQtEvents.OldSql = sqlQueryString ' Cache the original query string
QueryBuilder.BuildSQLQueryStringFromInterface interface, sqlQueryString
' Test message
MsgBox sqlQueryString
qt.CommandText = sqlQueryString
If Not qt Is Nothing Then
qt.Refresh
Else
' ... Error handling code here...
End If
''' CLEAN UP '''
' Free the dictionary
Set qtDict = Nothing
End Sub
Also here is a screenshot of the Module structure http://imgur.com/8fUcfLV
My first thought on what might be the issue was passing the QueryTable by value. I am not the most experienced VBA developer, but I reasoned this would create a copy and be calling the event on an unrelated table. However, this was not the case and passing by Reference did not fix the problem either.
Also the query is confirmed to run successfully as the data is correctly showing up and being refreshed.
EDIT
I added the BeforeRefresh event function to CQtEvents class Module and confirmed this function is called once Refresh is called
Private Sub mQryTble_BeforeRefresh(Cancel As Boolean)
MsgBox "Start of BeforeRefresh"
End Sub
How might I alter this code get my QueryTable from the QTableModule's RefreshDataQuery() Sub routine to have the AfterRefresh function invoked when the query is successfully ran?
How to catch the AfterRefresh event of QueryTable?
Explanation: in your situation, before event was fired you lost reference of your QueryTable by setting it to nothing when you made cleaning or procedure ended.
General solution: you must be sure that your code is still running and/or you need to keep any references to your QueryTable.
1st solution. When calling QT.Refresh method set the parameter to false in this way:
qt.Refresh false
which will stop further code execution until your qt is refreshed. But I don't consider this solution to be the best one.
2nd solution. Make your classQtEvents variable public and after RefreshDataQuery sub is finished check the status with some other code.
in you CQtEvents class module add the following public variable:
Public Refreshed As Boolean
in your BeforeRefresh event add this:
Refreshed = False
in your AfterRefresh event add this line of code:
Refreshed = True
Make your classQtEvents variable declaration public. Put this before Sub RefreshDataQuery()
Public classQtEvents as CQtEvents
but remove appropriate declaration from within your sub.
Now, even your sub is finished you will be able to check status of refreshment by checking .Refreshed property. You could do it in Immediate or within other Sub. This should work for Immediate:
Debug.Print classQtEvents.Refreshed
3rd solution. (a bit similar to 1st one) Follow steps 1 to 3 from 2nd solution. After you call qt.Refresh method you could add this loop which will stop further code execution until qt is refreshed:
'your code
If Not qt Is Nothing Then
qt.Refresh
Else
' ... Error handling code here...
End If
'checking
Do Until classQtEvents.Refreshed
DoEvents
Loop
Final remark. I hope I didn't mixed up qt variable with classQtEvents variable. I didn't tried and tested any solution using your variables but wrote all above with referenced to code I use.
A github repo that demonstrates the minimum code needed to get this working can be found here.
As mentioned, if your event handler isn't in scope, or your QueryTable reference is lost, you won't catch the event. The key factors to ensuring you catch the event are:
Declare a global variable of your event-handling class module's type outside of any subroutines/methods, at the top of a file (I chose the ThisWorkbook file).
Add a Workbook_Open event handler and instantiate that variable there, so that it is available immediately and will remain in scope (since it's global).
At that point, or at any downstream point when you have a QueryTable you're interested in, pass that QueryTable to the global instance to wire up its events.
(It took me a couple tries to figure this out myself, when someone pointed me in this direction as an answer to this question.)
Objective: Redirect focus from one command button to another using the first's GotFocus procedure.
Context: I have a form-independent procedure in a generic module that, on most forms, sets focus to the NewRecord button after saving the previous record. But on one form, I would like to redirect (based on certain conditions) focus back to the SignRecord button so the user can "sign" a second part of the same record (I may need this for other uses in the future). The target control is enabled and visible and can otherwise be focused and the original control can be focused when the redirect doesn't occur. Reference [2] below implies that this should be possible, though I'm not changing visibility of my controls.
Issue: When the conditions are met to redirect focus in the GotFocus procedure, it redirects as desired but the original (test) SetFocus call throws a "Run-time error '2110', Can't move focus to the control CommandNew".
What I've tried:
Exit Sub after my downstream SetFocus calls.
Call CommandSign.SetFocus in the hopes that it would make it happen outside the previous SetFocus process.
In a module,
Public Sub test()
Forms("TargetForm").CommandNew.SetFocus 'This gets the error '2110'
End Sub
In the 'TargetForm',
Private Sub CommandNew_GotFocus()
If IsNull(textDateTime) Then Exit Sub 'Works as expected
'I can see these two parts work. The framSign value changes
'and CommandSign gets focus
If checPPC And IsNull(textSigID_PPC) And framSign = 2 Then
framSign = 1
CommandSign.SetFocus
ElseIf checDAS And IsNull(textSigID_DAS) And framSign = 1 Then
framSign = 2
CommandSign.SetFocus
End If
End Sub
References:
[1]: SelectNextControl() a bad idea in a GotFocus event?
[2]: http://www.access-programmers.co.uk/forums/showthread.php?t=100071
I think your problem is that the call to Forms("TargetForm").CommandNew.SetFocus doesn't quite seem to, in fact, finish setting the focus to CommandNew until after Private Sub CommandNew_GotFocus() has finished executing. Because you've called another SetFocus before the first SetFocus could finish, there is a conflict that Access seems to be unable to cope with.
Whether or not that is the case, one thing is clear: the way you have your execution plan set up right now is unfortunately not going to work. You might try adding either a global variable or a public variable to each form that determines whether or not you should set your focus to CommandSign after you set the focus to CommandNew.
Ex. TargetForm:
Public boolSetCommandSignFocusInstead As Boolean
Private Sub CommandNew_GotFocus()
If IsNull(textDateTime) Then Exit Sub 'Works as expected
'I can see these two parts work. The framSign value changes
'and CommandSign gets focus
If checPPC And IsNull(textSigID_PPC) And framSign = 2 Then
framSign = 1
boolSetCommandSignFocusInstead = True
ElseIf checDAS And IsNull(textSigID_DAS) And framSign = 1 Then
framSign = 2
boolSetCommandSignFocusInstead = True
Else
boolSetCommandSignFocusInstead = False
End If
End Sub
Module:
Public Sub test()
Forms("TargetForm").CommandNew.SetFocus
If Forms("TargetForm").boolSetCommandSignFocusInstead Then
Forms("TargetForm").CommandSign.SetFocus
End If
End Sub