Trying to use Threading.Tasks.Task to kick off a background search while user types, but after they pause - vb.net

I've searched but I can't find the solution I'm looking for.
I specifically want to use Threading.Tasks.Task for this project, just to understand it better.
I have a lengthy XML file that I want to search based on text that a user types in. Because it's lengthy, I want to wait for the user to stop typing for 250ms or so before I actually start searching in the background. I tried to kick off my Task, having it sleep for 250ms, then check if my CancelTokenSource had canceled because another character was typed. I'm not seeing a cancellation, though, so I end up seeing my results flash several times as the queued up search tasks complete, one after the other, after I finish typing.
My code has turned into a terrible mess and I need to start over, but was hoping someone could point me in the right direction.

Start with a thread-safe property that determines when the search should begin. Initialise it to Date.MaxValue to prevent it running before it's asked to.
Private Property SearchTriggerTime As Date
Get
SyncLock SearchTriggerTimeLock
Return _SearchTriggerTime
End SyncLock
End Get
Set(value As Date)
SyncLock SearchTriggerTimeLock
_SearchTriggerTime = value
End SyncLock
End Set
End Property
Private _SearchTriggerTime As Date = Date.MaxValue
Private ReadOnly SearchTriggerTimeLock As New Object
When the search text box text changes, set the timer to when you want to start searching. As the user types quickly, the search timer will be reset before it triggers. In the code below, if the user clears the text box, the timer is set to never trigger, ie. do not search.
Private Const SearchDelay As Integer = 250
Private Sub TextBox1_TextChanged(sender As Object, e As EventArgs) Handles TextBox1.TextChanged
If TextBox1.Text <> "" Then
SearchTriggerTime = Date.Now.AddMilliseconds(SearchDelay)
Else
SearchTriggerTime = Date.MaxValue
End If
End Sub
Start a thread to perform the searching when the form loads.
Public Sub Form_Load(sender As Object, e As EventArgs) Handles Me.Load
With New Thread(AddressOf SearchThread)
.Start()
End With
End Sub
This thread passes through three states. The first state is waiting for the trigger time to start the search. It checks the trigger time every 50ms. The second state is performing the search. During the search, it checks if the form closes or if the user types more, and abandons the search in those cases. In the third state, if the search completes normally, the form's original thread is asked to display the results. If you need to change a control, always do so on the form's thread by using Form.Invoke(Delegate).
Private Sub SearchThread()
Do Until IsDisposed
' Wait for the user to stop typing.
Do Until IsDisposed OrElse SearchTriggerTime <= Date.Now
Thread.Sleep(50)
Loop
' Search until the form is disposed, the user types more, or the search is complete.
' TODO: Initialise the search variables.
Dim SearchComplete As Boolean = False
Do Until IsDisposed OrElse SearchTriggerTime > Date.Now OrElse SearchComplete
' TODO: Insert search code here.
If condition Then SearchComplete = True
Loop
' Reset the search timer.
SearchTriggerTime = Date.MaxValue
' Only display results if the search was completed.
If SearchComplete Then Invoke(New Action(AddressOf DisplaySearchResults))
Loop
End Sub
Lastly, create a method to display your search results. This will be run in the form's thread to prevent invalid cross-thread operations.
Private Sub DisplaySearchResults()
' TODO: Display search results.
End Sub

I managed to get it to work and it actually looks fairly clean (I think).
I'm using 'Static' variables so that I can keep all of the code within this method but still have my CancellationTokenSource available on subsequent calls.
I'm pretty pleased with the result, but still welcome comments and notes for improvement.
The actual search is actually happening on the UI thread (I think) but I'm doing that because it's easy to populate a treeview while I find valid nodes in my XML. My next refactor will be to do the search on a background thread (rather than just the 250ms wait) and use a TaskScheduler to post UI updates.
Private Sub txtQuickSearch_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles txtQuickSearch.TextChanged
Static scCancelTokenSource As Threading.CancellationTokenSource
Static scCancelToken As Threading.CancellationToken
If txtQuickSearch.Text.Length >= 3 Then
If scCancelTokenSource IsNot Nothing Then
scCancelTokenSource.Cancel()
scCancelTokenSource = Nothing
End If
scCancelTokenSource = New Threading.CancellationTokenSource
scCancelToken = scCancelTokenSource.Token
Dim lfSearch = Sub()
Dim ltToken As Threading.CancellationToken = scCancelToken
Threading.Thread.Sleep(250)
If Not ltToken.IsCancellationRequested Then
Me.Invoke(Sub() DoQuickSearch(txtQuickSearch.Text))
End If
End Sub
Threading.Tasks.Task.Factory.StartNew(lfSearch, scCancelToken)
End If
End Sub

Related

How to handle long running tasks in VB.NET forms?

I am currently working on a VB.NET form that automatically create Word documents according to an Excel file and a few extra data asked by the form (Project Name, Customer Name, Use SQL, ...).
This procedure works fine and takes approximatelly 1 or 2 minutes to complete.
The issue is that all my script is in ButtonGenerate.Click Handler. So when the Generate button is pressed the form window is bricked and it's impossible to Cancel...
It shouldn't be in a Click handler. Opening a new thread for that long task seems better. But Multithreading isn't very familiar to me.
I tryed launching the script with
ThreadPool.QueueUserWorkItem(...
but my Generate Sub sets labels and update a Progress Bar in the main form, so I doesn't work unless I use
Me.Invoke(New MethodInvoker(Sub()
label.Text = "..."
ProgressBar.Value = 10
' ...
End Sub)
each time I need to update something on the form and I can't even retrieve any new push of a button with that (A cancel button would be nice).
This is basically my code :
Public Class TestFichesAutomation
Private Sub BtnGenerate_Click(sender As Object, e As EventArgs) Handles BtnGenerate.Click
System.Threading.ThreadPool.QueueUserWorkItem(Sub() Generate())
End Sub
Public Sub Generate()
' Check user options, retrieve Excel Data, SQL, Fill in custom classes, create Word docs (~ 1 minute)
End Sub
So How would you handle that script ? Is Threading even a good solution ?
Thanks a lot for your help ^^ and for the useful doc.
My app now open a new thread and uses 2 custom classes to act like buffers :
Private Async Sub Btn_Click(sender As Object, e As EventArgs) Handles Btn.Click
myProgress = New Progress
' a custom class just for the UI with the current task, current SQL connection status and progress value in %
_Options.ProjectName = TextBoxProjectName.Text
_Options.CustomerName = TextBoxCustomerName.Text
...
' Fill in a custom "_Options" private class to act as a buffer between the 2 thread (the user choices)
Loading = New Loading()
Me.Visible = False
Loading.Show() ' Show the Loading window (a ProgressBar and a label : inputLine)
Task.Run(Function() Generate(Progress, _Options))
Me.Visible = True
End Sub
Public Async Function Generate(ByVal myProgress As Progress, ByVal Options As Options) As Task(Of Boolean)
' DO THE LONG JOB and sometimes update the UI :
myProgress.LoadingValue = 50 ' %
myProgress.CurrentTask= "SQL query : " & ...
Me.Invoke(New MethodInvoker(Sub() UpdateLoading()))
' Check if the task has been cancelled ("Cancelled" is changed by a passvalue from the Loading window):
If myProgress.Cancelled = True Then ...
' Continue ...
End Function
Public Shared Sub UpdateLoading()
MyForm.Loading.ProgressBar.Value = myProgress.LoadingValue
MyForm.Loading.inputLine.Text = myProgress.CurrentTask
' ...
End Sub
You should look into using the Async/Await structure
if the work you need to do is CPU bound, i like using Task.Run() doc here
By making your event handler Async and having it Await the work, you prevent the UI from locking up and avoid the use of Invoke in most cases.
ex:
Private Async Sub Btn_Click(sender As Object, e As EventArgs) Handles Btn.Click
Dim Result As Object = Await Task.Run(Function() SomeFunction())
'after the task returned by Task.Run is completed, the sub will continue, thus allowing you to update the UI etc..
End Sub
For progress reporting with Async/Await you might be interested in this

How to decrement the value of a label every three seconds (VB)

I am making a game for a piece of coursework and I have came across a problem. It is a very simplistic game. An enemy will spawn (Picture Box) and you will shoot it (Left Click) to make it die (Disappear). I want the user to lose 5 health for every 3 seconds the enemy is alive. The only way I could think of doing this is by using a timer and a text box. The timer is disabled when the game begins. When the enemy spawns the timer becomes enabled and the text box begins to increment by one every second. When the user kills the enemy the timer becomes disabled again and the text box is reset to 0. Now all I need to do is for the user to lose health every 3 seconds the enemy is alive. The following code is the code I currently have:
Private Sub timerenabled()
If PicBoxEnemy.Visible = True Then
Timer2.Enabled = True
Else
Timer2.Enabled = False
TxtBox.Text = 0
End If
End Sub
Private Sub Timer2_Tick(sender As Object, e As EventArgs) Handles Timer2.Tick
TxtBox.Text = TxtBox.Text + 1
End Sub
Private Sub checkvalue()
Dim x As Integer
x = TxtBox.Text / 3
If CInt(x) Then
Do
health = health - 5
Loop Until health = 0
End If
End Sub
Any other more efficient ways to do this would be appreciated. I hope you understand what I am trying to do.
First and foremost, Stack Overflow isn't really a tutorial site, but I can't resist answering you.
OK there are to be honest several issues with your code. But first, to your question. Instead of using a TextBox, Use a Label. The textbox could be modified by the user. This brings me to one of the issues.
First, It's really bad practice to use controls as the repository for data. You have the right idea with the variable health.
Second. Turn on Option Strict in Visual studio's settings. While you are there, make sure that Explicit is on, Compare is Binary, and Infer is Off.
Have a look at this Stack Overflow answer
Changing these options will mean that you will write less buggy code , but on the downside, you will need to write a bit more.
Finally take a little time to choose meaningful names for your variables and objects, it will make it a lot easier to remember what they're for. For example call Timer2 something like TmrGameRunning - Not something like TmrGR in six months time you probably wont remember what a name like that means. :-)
You'll need to create a label called LblHealth. I'm assuming that the TxtBox control can be discarded as it is merely there to count timer ticks. You don't need it. Also assuming that you added the timer as a Timer control, in the timer's properties, just set the interval to 3000 which is the number of milliseconds between ticks = 3 seconds
Have a look at the modified code and the comments for explanations
Public Class Form1
Dim health As Integer
' This will be the variable that note if your player is alive or dead .. True if alive, False if dead
Dim PlayerAlive As Boolean = True
'This is slightly different to your code. In VB, there is an event that will fire when the
'visibility of a textbox changes. The following method will execute when this happens. Just like code
'that you would write when you're handling a button.click event
Private Sub PicBoxEnemy_VisibleChanged(sender As Object, e As EventArgs) Handles PicBoxEnemy.VisibleChanged
If PicBoxEnemy.Visible = True Then
Timer2.Enabled = True
Else
Timer2.Enabled = False
End If
End Sub
'This is a modified version of your timer tick - Don't forget to change the timer .Interval property
'to 3000
Private Sub Timer2_Tick(sender As Object, e As EventArgs) Handles Timer2.Tick
health = health - 5
'This will change the text of the label to whatever your player's health is and the line below
'will force the label to update
LblHealth.Text = health.ToString
LblHealth.Update()
'Also while the timer is ticking the method below will check the health of your player and decide if
'they are dead or not. If they are, the timer is disabled and the PlayerDead method is called.
AliveOrDead()
End Sub
Private Sub AliveOrDead()
If health <= 0 Then
Timer2.Enabled = False
PlayerDead()
End If
End Sub
'This will be the method that executes when the player is dead. You'll need to add your own code
'for this of course, depending on what you want to do.
Private Sub PlayerDead()
'code here for what happens at the end of the game
End Sub
End Class
Hint. You'll probably need a button control and a Button.Click event handler method to start the game, a way of making the PictureBox visible (possibly at random intervals) while the game is running,(dont forget to stop this timer when the PictureBox is made visible), and finally an event handler that is called when you click on the picture to make it invisible(and stop the timer that reduces health)

Cross Thread Error Trying To Open New Form Instance Inside Timer

I am trying to create little notification popups for my application and have created a new form that fades in and out and sits on top of my main form (seems to work okay).
My problem is that I have some code that sits inside a timer event that does some data checking every minute or so. Depending on the data results, I sometimes need to show a notification. However, it is causing me Cross-Thread errors (which is understandable), but I'm not sure how to get around it.
Example (in a nutshell) of what I am trying to do is:
Private Sub RefreshData(sender As Object, e As System.Timers.ElapsedEventArgs)
Try
MainRefreshTimer.Interval = GetInterval()
MainRefreshTimer.Start()
'Do some data checking here...
If data returns true then
Dim notify as New frmNewNotification("Some Text", 10) '<== Show some text for 10 seconds then close the form automatically
notify.Show() '<== Cross Thread Error occurs from this
End If
...
End Sub
I would try one of this ideas:
Shorcut: Put this in your Form_Load
Control.CheckForIllegalCrossThreadCalls = False
Or, better, something like:
Private Sub delRefreshData(data as Object)
If Me.InvokeRequired Then
' Invoke(New MethodInvoker(AddressOf delRefreshData)) ' no params
Invoke(New MethodInvoker(Sub() delRefreshData(data)))
Else
'Do some data checking here...
If data returns true then
Dim notify as New frmNewNotification("Some Text", 10)
notify.Show() '
End If
End if
Using InvokeRequired vs control.InvokeRequired
Edited:
To avoid in future be blamed for that Shorcut, I have to say that
Control.CheckForIllegalCrossThreadCalls isn't a good advice/solution, as is discussed here:
Cross-thread operation not valid: Control accessed from a thread other than the thread it was created on

Making a button.click event do two different things

I'm working on a simple VB.NET program (just using winforms) and am really terrible at UI management. I'd like to have a single button that starts a process, and then have that same button stop the process.
I'm thinking about having the main form initiate a counter, and the Click event iterate the counter. Then it does a simple check, and if the counter is even it will do thing A and odd does thing B.
Is there a better way, aside from using two buttons or stop/start radio buttons?
I've done that exact thing one of two ways. You can use a static variable or toggle the text of the button.
Since your button has two functions, Good design requires you to indicate that to the user. The following code assumes the Button's text is set in Design Mode to "Start", and the code to start and stop your process is in the Subs StartProcess and EndProcess.
Public Sub Button1_Click(ByVal Sender as Object, ByVal e as System.EventArgs)
If Button1.Text ="Start" Then
StartProcess()
Button1.Text="End"
Else
EndProcess()
Button1.Text="Start"
End IF
End Sub
EDIT
The above solution is fine for a single-language application developed by a small number of developers.
To support multiple languages, developers typically assign all text literals from supporting files or databases. In larger development shops, with multiple programmers, using a display feature of the control for flow-control may cause confusion and regression errors. In those cass, the above technique wouldn't work.
Instead, you could use the Tag property of the button, which holds an object. I would typically use a Boolean, but I used a string just to make more clear as to what's going on.
Public Sub New()
'Initialize the Tag
Button1.Tag="Start"
End Sub
Public Sub Button1_Click(ByVal Sender as Object, ByVal e as System.EventArgs)
If Button1.Tag.ToString="Start" Then
StartProcess()
Button1.Tag="End"
Else
EndProcess()
Button1.Tag="Start"
End IF
End Sub
This is example in pseudo-code. I don't guarantee that names of methods and event are exactly match real names. But this should provide you a design that you could use for responsive form.
Lets say, your process is running on separate tread, using BackgroundWorker.
You setup your worker and start process
Class MyForm
private _isRunning as boolean
private _bgWorker as BackgroundWorker
sub buton_click()
If Not _isRunning Then
_isRunning = true;
StartProcess()
Else
StopProcess()
End if
end sub
sub StartProcess()
' Setup your worker
' Wire DoWork
' Wire on Progress
' wire on End
_bgWorker.RunWorkerAsync()
End sub
sub StopProcess()
if _isRunning andAlso _bgWorker.IsBusy then
' Send signal to worker to end processed
_bgWorker.CancelAsync()
end if
end sub
sub DoWork()
worker.ReportProgress(data) ' report progress with status like <Started>
' periodically check if process canceled
if worker.canceled then
worker.ReportProgress(data) ' report progress with status like <Cancelling>
return
end if
' Do your process and report more progress here with status like <In Progress>
' and again periodically check if process canceled
if worker.canceled then
worker.ReportProgress(data) ' report progress with status like <Cancelling>
return
end if
worker.ReportProgress(data) ' report progress with status like <Ending>
end sub
sub ReportProgress(data)
if data = <some process state, like "Started"> then
btnProcess.Text = "End Process"
end if
End sub
sub ReportEndOfProcess
btnProcess.Text = "Start Process"
_isRunning = false
end sub
End Class
Here you can pinpoint the names of methods and events
You have to substitute identifiers with real names and create you state or data object, which will carry information from background thread to UI thread, and also an Enum Status that can be part of your custom state object. This should work once translated into real code
Just want to show another approach for this task
Use .Tag property for your own purpose
If .Tag Is Nothing (by default in designer) then start process
If not Nothing -> stop process
Public Sub Button1_Click(ByVal Sender as Object, ByVal e as System.EventArgs)
If Me.Button1.Tag Is Nothing Then
StartProcess()
Me.Button1.Tag = New Object()
Me.Button1.Text = "End"
Else
EndProcess()
Me.Button1.Tag = Nothing
Me.Button1.Text = "Start"
End
End Sub

vb.net problems with invoke

I have a thread that runs background jobs and is required to update the GUI once in a while. My program has been designed so that when the user clicks off of a form, the thread and background operations still run, yet the controls have been disposed (for memory management purposes).
I have been using Invoke() and "If Control.Created = True" to make sure that the thread can successfully update the controls without running into any exceptions. However, when the form is recreated, all "Control.Created" values are false and Invoke() fails with "{"Invoke or BeginInvoke cannot be called on a control until the window handle has been created."}"
My guess is that this has something to do with the fact that when the form is recreated it is assigned different handles and that the "Invoke()" is looking at the old handle. SO my question is, how do I fix this?
EDIT: As per requested, the code for opening the form and where the bg thread works from
Opening the DropLogMDIalt form is simply
FormCTRL.Show()
The Background Thread runs when the control is modified so that the NumericUpDown is more than 0 (so that there is something to countdown from)
Private Sub NLauncherTerminateInput_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles DLScanInterval.ValueChanged
If DLScanInterval.Created = True Then
DLTimerControlValue = DLScanInterval.Value
If DLTimerControlValue = 0 Then
CancelDropLogTimer()
Else
If DLScanIntervalControl.Active = False Then
BeginDropLogTimer()
End If
End If
End If
End Sub
Public Sub BeginDropLogTimer()
Dim N As New Threading.Thread(AddressOf DropLogTimerIntervalThreadWorker)
N.Start()
DLScanIntervalControl.ThreadID = N.ManagedThreadId
DLScanIntervalControl.Active = True
End Sub
Public Sub CancelDropLogTimer()
DLScanIntervalControl.Active = False
End Sub
Public Sub DropLogTimerIntervalThreadWorker()
DLScanTimerSecondsLeft = DLTimerControlValue * 60
Dim s As Integer = DLTimerControlValue
Do Until 1 = 2
DLScanTimerSecondsLeft = DLTimerControlValue * 60
Do Until DLScanTimerSecondsLeft <= 0
If Not (DLTimerControlValue = 0 Or DLScanIntervalControl.CancelPending = True) Then
Else
Exit Sub
End If
If Not DLTimerControlValue = s Then
DLScanTimerSecondsLeft = DLTimerControlValue * 60
s = DLTimerControlValue
End If
Dim ToInvoke As New MethodInvoker(Sub()
Timer(DLScanTimerSecondsLeft, ":", DLScanIntervalTB)
End Sub)
If (Me.IsHandleCreated) Then
If (InvokeRequired) Then
Invoke(ToInvoke)
Else
ToInvoke()
End If
End If
Threading.Thread.Sleep(1000)
DLScanTimerSecondsLeft -= 1
Loop
CompareScan = True
PerformScan()
Loop
End Sub
The thread is simply called by declaring a new thread.thread, however, I have created a class and a variable that the thread uses to check if it should still be running or not (similarly to how a backgroundworker would) this is illustrated by the "DLScanIntervalControl.CancelPending"
The form is then closed later by
Form.Close()
It can be reopened if the user clicks on a label that then uses the same method as shown above (FormCTRL.Show())
From MSDN:
"If the control's handle does not yet exist, InvokeRequired searches up the control's parent chain until it finds a control or form that does have a window handle. If no appropriate handle can be found, the InvokeRequired method returns false."
In other words, you need to verify that the handle is created and then check if invoke is required.
If(Me.IsHandleCreated) Then
If(Me.InvokeRequired) Then
'...
Else
'...
End If
End If
I had a similar error when trying to use delegates to update controls on a form in another thread. I found that the handle is only created when it's "needed". I'm not sure what constitutes "needed", but you can force it to create the handle by accessing the Handle property of the object.
What I had done in my application is this:
' Iterate through each control on the form, and if the handle isn't created yet, call the
' Handle property to force it to be created
For Each ctrl As Control In Me.Controls
While Not ctrl.IsHandleCreated
Dim tmp = ctrl.Handle
tmp = Nothing
End While ' Not ctrl.IsHandleCreated
Next ' ctrl As Control In Me.Controls
It's rather ghetto, but it may help you here (If you still need the help)
I think the issue here has nothing to do with invoking, but references. Following this pseudocode...
Dim A As New Form1
A.Show()
''Spawn background thread with a reference to A
A.Dispose()
Dim B As New Form1
B.Show()
The thread is attempting to refer to the first instance of Form1 above which is disposed and will always stay that way.
If you want the thread to be able to update any form then you need to give the thread a (synchronised) way to refer to the form...
Public Class Worker
Private Target As Form1
Private TargetLock As New Object
Public Sub SetTargetForm(Frm as Form1)
SyncLock TargetLock
Target = Frm
End SyncLock
End Sub
Public Sub DoWork() ''The worker thread method
''Do work as usual then...
SyncLock TargetLock
If Target IsNot Nothing AndAlso Target.IsHandleCreated Then
If Target.InvokeRequired
Target.Invoke(...)
Else
...
End If
End If
End SyncLock
End Sub
End Class
This way, when a new form is available, you can inform the worker thread using SetTargetForm() and it will update the appropriate one.
Of course, you'd be better off refactoring the "Update UI" checks and invoke calls into a different method for simplicity/maintainability but you get the point.
Note that I haven't got an IDE to hand so there may be typos.
One final point... I'd question the value of disposing a form for memory management purposes. Controls are fairly lightweight in terms of memory and it's far more common for an object used by the form to be a memory hog than the form itself. Are you sure you're getting a real benefit for this added complexity?