How to decrement the value of a label every three seconds (VB) - vb.net

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)

Related

Most efficient way to programmatically update and configure forms and controls

I am looking for a way to prevent user form controls from appearing one by one when I'm programmatically adding them and for ways to enhance application performance and visual appeal.
Say, I have a Panel_Top in which I programmatically add comboboxes. What is happening is that they appear one by one as they are created and I am looking for a way to suspend the refreshing of the panel and or user form to make all of those programmatically added comboboxes to appear at the same time and faster than it happens right now.
I've tried suspendlayout which doesn't do anything for me or maybe I'm doing it wrong.
MyForm.PanelTop.SuspendLayout = true
And also I've tried to set the Panel_Top to invisible like:
MyForm.Top_Panel.visible = false
Which kind of sorta looks and performs better, or it might be a placebo.
What is the correct approach to this problem?
PS: I do have form set to doublebuffer = true, if that matters
What I tend to do is create a loading modal to appear on top of the form rendering the controls that need to be created/made visible, this can optionally have a progress bar that gets incremented as the control is created/shown. With the loading modal running, the container that needs to add the controls starts with SuspendLayout, adds the controls, and then finished with ResumeLayout.
This makes it so that controls are added/shown while giving the user a visual indicator that something is going on behind the scenes.
Here is a phenomenal example of a loading modal: https://www.vbforums.com/showthread.php?869567-Modal-Wait-Dialogue-with-BackgroundWorker and here is an example of using it:
Private ReadOnly _controlsToAdd As New List(Of Control)()
Private Sub MyForm_Show(sender As Object, e As EventArgs) Handles MyBase.Shown
Using waitModal = New BackgroundWorkerForm(AddressOf backgroundWorker_DoWork,
AddressOf backgroundWorker_ProgressChanged,
AddressOf backgroundWorker_RunWorkerCompleted)
waitModal.ShowDialog()
End Using
End Sub
Private Sub backgroundWorker_DoWork(sender As Object, e As DoWorkEventArgs)
Dim worker = DirectCast(sender, BackgroundWorker)
For index = 1 To 100
_controlsToAdd.Add(New ComboBox() With {.Name = $"ComboBox{index}"})
worker.ReportProgress(index)
Threading.Thread.Sleep(100) ' Zzz to simulate a long running process
Next
End Sub
Private Sub backgroundWorker_ProgressChanged(sender As Object, e As ProgressChangedEventArgs)
Dim percentageCompleted = e.ProgressPercentage / 100
' do something with the percentageCompleted value
End Sub
Private Sub backgroundWorker_RunWorkerCompleted(sender As Object, e As RunWorkerCompletedEventArgs)
PanelTop.SuspendLayout()
PanelTop.Controls.AddRange(_controlsToAdd.ToArray())
PanelTop.ResumeLayout()
End Sub
SuspendLayout() is the correct way to handle this with WinForms.
But first of all, this is a function you call, and not a flag you set.
Secondly, don't forget to call ResumeLayout() at the end of the changes.
Finally, you need to ensure you only call them once when you start to change around the controls in the panel and then again at the very end. If you use them with every control you won't get any benefit.
So the pattern might look something like this:
Public Sub SomeMethod()
PanelTop.SuspendLayout() ' Prevent the panel from updating until you've finished
' Make a bunch of changes
PanelTop.Controls.Clear()
For Each ...
PanelTop.Controls.Add( ... )
Next
PanelTop.ResumeLayout() ' Allow the panel to show all the changes in the same WM_PAINT event
End Sub
You also need to ensure you don't have anything in there like DoEvents()/Invalidate() that might invoke the windows message loop and cause the form to redraw itself.

(VB.NET) Quick way for a Simple Splashscreen for WinForms [duplicate]

This question already has answers here:
How to create a Splash screen for VB.net program
(3 answers)
Closed 6 days ago.
My program took ~5-10 seconds to load and sometimes people using it would end up trying to open it again, which caused problems. I found a quick and easy way to make a "splashscreen" (in a sense) that pops up for a set amount of time immediately on execution. I found that the first order of events in a WinForm EXE loading was Handle Created. The answer is not a true splashscreen, but for a couple lines of code that can be easily added to a project, I think some people will like it.
The below code will show a MessageBox immediately on running the EXE and closes after 10 seconds.
Imports System.Threading
Private Sub Control1_HandleCreated(ByVal sender As Object, ByVal e As EventArgs) Handles Me.HandleCreated
Dim SplashScreen As New Thread(
Sub()
CreateObject("WScript.Shell").Popup("Program Initializing, Please Wait...",10, "Setup Tool")
End Sub)
SplashScreen.Start()
End Sub
I use Threading so that the MessageBox will not freeze the code and the program will open with or without the OK button being pressed. Doing a regular MessageBox.Show() will prevent any more code from running until the user clicks OK I have found.
The best way I have found to implement a splash screen which keeps the user informed via messages and/or a progress bar or animated wheel is the following.
Have a startup form eg Form1, and have it carry out all the tedious startup procedures which might cause any animated or progress bar graphic to get stalled in the event queue. Add a "BackgroundWorker" object to Form1 from the Toolbox and in my case I just named it BackgroundWorker1.
Before starting these routines, usually in the Form1_Load event, make a call to the BackgroundWorker.
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles Me.Load
CallBackgroundWork()
StartRoutines() 'this is the heavy lifting routines to get the app working. Set the LoadingStatusflag (declared as a Global Variable"
to various values to tell the splashscreen to display different messages
Loadingstatus = 10 'triggers splashform to exit
CancelBackgroundWork()
End Sub
These are the other subs to support this
Sub CallBackgroundWork()
BackgroundWorker1.WorkerSupportsCancellation = True
BackgroundWorker1.WorkerReportsProgress = True
' call this method to start your asynchronous Task.
BackgroundWorker1.RunWorkerAsync()
End Sub
Sub CancelBackgroundWork()
' to cancel the task, just call the BackgroundWorker1.CancelAsync method.
BackgroundWorker1.CancelAsync()
End Sub
Sub BackgroundWorker1_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
'' The asynchronous task we want to perform goes here
FormSplash.Show()
End Sub
My splashscreen has some label controls and pictureboxes and the FormSplash_Load event runs a stopwatch loop of 40ms and loads a series of images (24 in total) of a spinning wheel. This keeps running while the splashscreen is active. By setting the global variable Loadingstatus to various values within different part of the loading sequence in Form1 it can trigger the loop routine to display different messages example shown. An easy way to communicate between threads as you can't directly access objects between threads The wheel keeps spinning no matter how intensive the load routine in Form1 as it is running in another thread. I used a stopwatch loop as starting a timer doesn't work for me - maybe an event queue issue in splash form.
Private Sub FormSplash_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Me.Show()
Me.Opacity = 1 'show this form
'now start a loop that gets ended by other thread through variable Loadingstatus flag
Dim ggtimer As New Stopwatch, lastvalue As Integer, FProgPosition as integer
ggtimer.Start()
lastvalue = ggtimer.ElapsedMilliseconds
nextimage:
FProgPosition += 1
If FProgPosition = 24 Then FProgPosition = 1 'has 24 frames in the animated image
Do 'loop for 40 ms
If ggtimer.ElapsedMilliseconds - lastvalue > 40 Then
lastvalue = ggtimer.ElapsedMilliseconds
Exit Do
End If
Loop
PictureBoxProgress1.Image = FProgIMG(FProgPosition)
PictureBoxProgress1.Refresh()
If Loadingstatus = 10 Then GoTo endsplash
If Loadingstatus = 1 Then
If CoreTempRunning = False Then
Me.LabelCoreTemp.Text = "CoreTemp is NOT Running"
Me.LabelCoreTemp.ForeColor = Color.White
'insert cross picturebox
PictureBoxCoreTemp.Image = My.Resources.ResourceManager.GetObject("Cross24x24")
loaderrorflag2 = True
Else
Me.LabelCoreTemp.Text = "CoreTemp is Running"
Me.LabelCoreTemp.ForeColor = Color.White
'insert tick picturebox
PictureBoxCoreTemp.Image = My.Resources.ResourceManager.GetObject("Tick24x24")
loaderrorflag2 = False
End If
Me.PictureBoxCoreTemp.Visible = True
Me.PictureBoxCoreTemp.Refresh()
Me.LabelCoreTemp.Left = Me.Width * 2 / 3 - Me.LabelCoreTemp.Width
Me.LabelCoreTemp.Refresh()
GoTo nextimage
endsplash:
ggtimer.Stop()
Me.Opacity = 0.01
Me.Hide()
End Sub

Better solution, timer, stopwatch, timespan

I am working on small tool for tracking duration of various activities.
In this example we have 3 activities, Drive, Walk and Wait.
Each activitiy is a button on Form1
Example:
Click on button Drive, stopwatch "SW" and timer "Tmr" are started and counting "Drive" time.
After 5 seconds I click on button Wait, SW and Tmr are stopped, SW1 and Tmr1 are started and counting time for "Wait" activity.
Click again on button Drive, SW1 and Tmr1 as stopped, SW and Tmr started and time is resumed from 5th second
And so on, can be one or more activities included. At the end of measuring I have total duration for each activity.
This Code below is actually working well. Function is called from the Form1, measuring is started and later I have values in public variables available.
Module:
Dim SW, SW1, SW2 As New Stopwatch
Dim WithEvents Tmr, Tmr1, Tmr2 As New Timer
Dim stws() = {SW, SW1, SW2}
Dim tmrs() = {Tmr, Tmr1, Tmr2}
Public Drive, Walk, Wait As String
Public Function WhichButton(btn As Button)
WhichButton = btn.Text
Select Case WhichButton
Case "Drive"
For Each s As Stopwatch In stws
s.Stop()
Next
For Each t As Timer In tmrs
t.Stop()
Next
SW.Start()
Tmr.Start()
Case "Wait"
For Each s As Stopwatch In stws
s.Stop()
Next
For Each t As Timer In tmrs
t.Stop()
Next
SW.Start()
Tmr1.Start()
Case "Walk"
For Each s As Stopwatch In stws
s.Stop()
Next
For Each t As Timer In tmrs
t.Stop()
Next
SW2.Start()
Tmr2.Start()
End Select
End Function
Private Sub Tmr_Tick(sender As Object, e As System.EventArgs) Handles Tmr.Tick
Dim elapsed As TimeSpan = SW.Elapsed
Drive = $"{elapsed.Hours:00}:{elapsed.Minutes:00}.{elapsed.Seconds:00}"
End Sub
Private Sub Tmr1_Tick(sender As Object, e As System.EventArgs) Handles Tmr1.Tick
Dim elapsed As TimeSpan = SW1.Elapsed
Walk = $"{elapsed.Hours:00}:{elapsed.Minutes:00}.{elapsed.Seconds:00}"
End Sub
Private Sub Tmr2_Tick(sender As Object, e As System.EventArgs) Handles Tmr2.Tick
Dim elapsed As TimeSpan = SW2.Elapsed
Wait = $"{elapsed.Hours:00}:{elapsed.Minutes:00}.{elapsed.Seconds:00}"
End Sub
Reason im here is because I'm not happy with this solution and I don't have a knoweledge for advanced one. The probem here is that I can have X number of Buttons, can add new or remove few, it depends on situation, and I don't want to write block of Code for each. Also if I Change a text property of the button, Select Case will not work.
So I want to create timers and stopwatches dynamically for each button.
I would like to start with this:
Dim timers As List(Of Timer) = New List(Of Timer)
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
For Each btn As Button In Panel1.Controls.OfType(Of Button)
timers.Add(New Timer() With {.Tag = btn.Name})
AddHandler btn.Click, AddressOf Something
Next
End Sub
Public Sub Something(sender As Object, e As EventArgs)
Dim btn = DirectCast(sender, Button)
Dim tmr As Timer = timers.SingleOrDefault(Function(t) t.Tag IsNot Nothing AndAlso t.Tag.ToString = btn.Name)
End Sub
Here I can refer to Timer over the Tag property but I have no idea how to implement stopwatch and timespan.
Thanks for reading and any help is appreciated, suggestions, pseudocode, code examples.
Firstly, there's no point using three Timers. A single Timer can handle all three times. Secondly, based on what you've posted, there's no point using any Timer. The only reason I could see that a Timer would be useful would be to display the current elapsed time in the UI constantly, but you're not doing that. Repeatedly setting those String variables is pointless if you're not going to display them. Just get the Elapsed value from the appropriate Stopwatch if and when you need it.
As for your Buttons' Click event handler, it's terrible too. The whole point of a common event handler is because you want to do the same thing for each object so you only have to write the code once. If you end up writing separate code for each object in that common event handler then that defeats the point and makes your code more complex instead of less. You should be using separate event handlers for each Button.
If you were going to go with a common event handler though, at least extact out the common code. You have the same two For Each loops in all three Case blocks. That should be done before the Select Case and then only start the appropriate Stopwatch in each Case.
I don't think that you should be using Buttons though. You should actually be using RadioButtons. You can set their Appearance property to Button and then they look just like regular Buttons but still behave like RadioButtons. When you click one, it retains the depressed appearnce to indicate that it is checked and clicking a different one will release the previously-depressed one. In that case, your code might look like this:
Private ReadOnly driveStopwatch As New Stopwatch
Private ReadOnly waitStopwatch As New Stopwatch
Private ReadOnly walkStopwatch As New Stopwatch
Private Sub driveRadioButton_CheckedChanged(sender As Object, e As EventArgs) Handles driveRadioButton.CheckedChanged
If driveRadioButton.Checked Then
driveStopwatch.Start()
Else
driveStopwatch.Stop()
End If
End Sub
Private Sub waitRadioButton_CheckedChanged(sender As Object, e As EventArgs) Handles waitRadioButton.CheckedChanged
If waitRadioButton.Checked Then
waitStopwatch.Start()
Else
waitStopwatch.Stop()
End If
End Sub
Private Sub walkRadioButton_CheckedChanged(sender As Object, e As EventArgs) Handles walkRadioButton.CheckedChanged
If walkRadioButton.Checked Then
walkStopwatch.Start()
Else
walkStopwatch.Stop()
End If
End Sub
Because checking a RadioButton automatically unchecks any other, each CheckedChanged event handler only has to worry about its own Stopwatch.
If you wanted to display the elapsed time for a particular Stopwatch when it stops, you do that when it stops, e.g.
Private Sub driveRadioButton_CheckedChanged(sender As Object, e As EventArgs) Handles driveRadioButton.CheckedChanged
If driveRadioButton.Checked Then
driveStopwatch.Start()
Else
driveStopwatch.Stop()
driveLabel.Text = driveStopwatch.Elapsed.ToString("hh\:mm\:ss")
End If
End Sub
That overload of TimeSpan.ToString was first available in .NET 4.5 I think, so you should use it unless you're targeting .NET 4.0 or earlier.
If you did want to display the current elapsed time constantly then, as I said, you only need one Timer. You would just let it run all the time and update appropriately based on the Stopwatch that is currently running, e.g.
Private Sub displayTimer_Tick(sender As Object, e As EventArgs) Handles displayTimer.Tick
If driveStopwatch.IsRunning Then
driveLabel.Text = driveStopwatch.Elapsed.ToString("hh\:mm\:ss")
ElseIf waitStopwatch.IsRunning Then
waitLabel.Text = waitStopwatch.Elapsed.ToString("hh\:mm\:ss")
ElseIf walkStopwatch.IsRunning Then
walkLabel.Text = walkStopwatch.Elapsed.ToString("hh\:mm\:ss")
End If
End Sub
You haven't shown us how you're displaying the elapsed time so that's a bit of a guess. In this scvenario, you should definitely still update the Label when a Stopwatch stops, because the Timer won't update that Label on the next Tick.
You would presumably want a Button somewhere that could stop and/or reset all three Stopwatches. That would mean setting Checked to False on all three RadioButtons and then calling Reset on all three Stopwatches. You'll probably want to clear/reset the Labels too.
There's also a potential gotcha using RadioButtons like this. If one of your RadioButtons is first in the Tab order then it will recieve focus by default when you load the form. Focusing a RadioButton will check it, so that would mean that you'd start a Stopwatch by default. If that's not what you want, make sure that some other control is first in the Tab order. If you can't do that for some reason, handle the Shown event of the form, set ActiveControl to Nothing, uncheck that RadioButton and reset the corresponding Stopwatch and Label.
As a final, general message, notice that I have named everything so that even someone with no prior knowledge of the project would have no doubt what everything was and what it was for. Names like SW, SW1 and SW2 are bad. Even if you realised that SW meant Stopwatch, you have no idea what each one is actually for. In this day of Intellisense, it's just lazy use names like that. Every experienced developer can tell you a story about going back to read their own code some time later and having no idea what they meant by various things. Don't fall into that trap and make sure that you get into good habits early.
EDIT:
As a bonus, here's a way that you can use a common event handler properly. Firstly, define a custom Stopwatch class that has an associated Label:
Public Class StopwatchEx
Inherits Stopwatch
Public Property Label As Label
End Class
Once you make that association, you automatically know which Label to use to display the elapsed time for a Stopwatch. Next, define a custom RadioButton class that has an associated Stopwatch:
Public Class RadioButtonEx
Inherits RadioButton
Public Property Stopwatch As StopwatchEx
End Class
Next, use that custom class on your form instead of standard RadioButtons. You can add them directly from the Toolbox (your custom control will be added automatically after building your project) or you can edit the designer code file and change the type of your controls in code. There is a certain amount of risk in the latter option so be sure to create a backup beforehand. Once that's all done, change the type of your Stopwatches and handle the Load event of the form to create the associations:
Private ReadOnly driveStopwatch As New StopwatchEx
Private ReadOnly waitStopwatch As New StopwatchEx
Private ReadOnly walkStopwatch As New StopwatchEx
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
'Associate Stopwatches with RadioButtons
driveRadioButton.Stopwatch = driveStopwatch
waitRadioButton.Stopwatch = waitStopwatch
walkRadioButton.Stopwatch = walkStopwatch
'Associate Labels with Stopwatches
driveStopwatch.Label = driveLabel
waitStopwatch.Label = waitLabel
walkStopwatch.Label = walkLabel
End Sub
You can now use a single method to handle the CheckedChanged event for all three RadioButtons because you can now do the exact same thing for all three of them:
Private Sub RadioButtons_CheckedChanged(sender As Object, e As EventArgs) Handles driveRadioButton.CheckedChanged,
waitRadioButton.CheckedChanged,
walkRadioButton.CheckedChanged
Dim rb = DirectCast(sender, RadioButtonEx)
Dim sw = rb.Stopwatch
If rb.Checked Then
sw.Start()
Else
sw.Stop()
sw.Label.Text = sw.Elapsed.ToString("hh\:mm\:ss")
End If
End Sub
The RadioButton that raised the event tells you which Stopwatch to use and that tells you which Label to use, so there's no need to write different code for each one.
The Tick event handler of the Timer can also treate each Stopwatch with common code:
Private Sub displayTimer_Tick(sender As Object, e As EventArgs) Handles displayTimer.Tick
For Each sw In {driveStopwatch, waitStopwatch, walkStopwatch}
If sw.IsRunning Then
sw.Label.Text = sw.Elapsed.ToString("hh\:mm\:ss")
Exit For
End If
Next
End Sub
You can create the array atthe class level but, as it's only being used in this one place, it makes sense to create it here. The performance hit is insignificant and it makes the code more readable by creating things where they are used.
Note that I did use abbreviations for variable names in this code. That's for two reasons. Firstly, they are variables that will refer to different objects at different times. That means that using a name specific to the purpose of the object is not possible. You could use a context-based name, e.g. currentRadioButton, but I don't do that here because of the second reason.
That second reason is that they are local variables used in a very limited scope. The rb and sw variables are not used more than a few lines from where they are declared so it's hard to not understand what they are. If you name a field like that then, when you see it in code, you have to look elsewhere to find out what it is. In this code, if you're looking at a usage of one of those variables then the declaration is in eyeshot too, so you'd have to be blind not to see what type you're dealing with. Basically, if a variable is used a long way from its declaration then I suggest a meaningful, descriptive name. If it is only used within a few lines of its declaration though, a brief name is OK. I generally tend to use the initials of the type, as I have done here. If you need multiple local variables of that type, I generally prefer to use descriptive names to disambiguate them rather than using numbers. Sometimes, though, there's really no purpose-specific way to do that, in which case numbers are OK, e.g. comparing two Strings without context might use s1 and s2 as variable names.

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

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

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