I've confusing with using command line argument in VB.net Winform.
I'm created an application integrated with dlsrbooth. Dlsrbooth send a trigger via command line argument. (https://support.lumasoft.co/hc/en-us/articles/360000637854-Triggers-Webhooks-and-API)
dlsrbooth will send trigger 'session_end' at first time running and change trigger after we start to take a photo session.
Since dlsrbooth screen based trigger (when take picture, will be send another trigger) so I use timer to read the argument.
If I put check argument at main form (form 1), I can read each trigger. But if I put read argument at 4th form, I didn't receive next screen trigger, only first time running trigger (session_end trigger).
Imports System.Reflection.Emit
Public Class Form6
Private TargetDT As DateTime
Private TargetDT2 As DateTime
Private CountDownFrom As TimeSpan = TimeSpan.FromSeconds(60)
Private startFrom As TimeSpan = TimeSpan.FromSeconds(10)
Dim wait As TimeSpan
Public Sub checkstatus()
If (Val(Form6.Label1.Text) <= 0) Then
Try
Dim sCmdLine As String() = Environment.GetCommandLineArgs()
Using sw As New StreamWriter(strFile)
sw.WriteLine(sCmdLine(1))
sw.Flush()
End Using
Catch
End Try
End If
End Sub
Private Sub tmrCountdown_Tick(sender As Object, e As EventArgs) Handles tmrCountdown.Tick
Dim ts As TimeSpan = TargetDT.Subtract(DateTime.Now)
wait = TargetDT2.Subtract(DateTime.Now)
checkstatus()
Label1.Text = wait.Seconds.ToString
If ts.TotalMilliseconds > 0 Then
lbltimer.Text = ts.ToString("mm\:ss")
Else
lbltimer.Text = "00:00"
tmrCountdown.Stop()
Form1.Show()
Me.Close()
End If
End Sub
Private Sub Form6_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Width = lbltimer.Width
Height = lbltimer.Height
tmrCountdown.Interval = 1000
TargetDT = DateTime.Now.Add(CountDownFrom)
TargetDT2 = DateTime.Now.Add(startFrom)
tmrCountdown.Start()
End Sub
End Class
I already try put checkstatus in main form (form 1) and call it within timer in 4th form, but the result is same, can't read next screen trigger.
Is it possible to read command line argument on other form (not main form)? How? Or where form event that should I put the command line argument?
Note: I want to send "session_start" command via API but it's not working, so I'm delaying read trigger and read each second using timer.
Related
I am working on a project where the user sets a reminder with information, date and time for when a notification should pop up and the notification should be clickable and open another form with the reminder information on it. So far I can set the reminder but it pops up as soon as the remind button in clicked, my notification looks a standard windows 10 notification, I just want the notification to be scheduled for a certain date and time. The information including date and time is saved into an Access Database. I am using VB.Net
Kind Regards
Form:
This is my reminder form as of now
This is how my notification looks like
Imports System.Data.OleDb
Public Class frmReminder
Private CurrentReminderID As Integer = -1
Private Sub frmReminder_Load(sender As Object, e As EventArgs) Handles MyBase.Load
BtnClear.PerformClick()
Timer1.Enabled = True
End Sub
Private Sub BtnClear_Click(sender As Object, e As EventArgs) Handles BtnClear.Click
Label6.Text = ""
TxtCustName.Text = ""
TxtDeviceInfo.Text = ""
TxtPrice.Text = ""
TxtDateDue.ResetText()
End Sub
Private Sub BtnSetReminder_Click(sender As Object, e As EventArgs) Handles BtnSetReminder.Click
If DbConnect() Then
Dim SQLCmd As New OleDbCommand
If CurrentReminderID = -1 Then
With SQLCmd
.Connection = cn
.CommandText = "Insert into TblReminder (CustomerName, DeviceInfo, RepairPrice, ReminderDate)"
.CommandText &= "Values (#CustomerName, #DeviceInfo, #RepairPrice, #ReminderDate)"
.Parameters.AddWithValue("#CustomerName", TxtCustName.Text)
.Parameters.AddWithValue("#DeviceInfo", TxtDeviceInfo.Text)
.Parameters.AddWithValue("#RepairPrice", TxtPrice.Text)
.Parameters.AddWithValue(" #ReminderDate", TxtDateDue.Text)
.ExecuteNonQuery()
.CommandText = "Select ##Identity"
CurrentReminderID = .ExecuteScalar
Label6.Text = CurrentReminderID
End With
End If
End If
Notification.ShowBalloonTip(1000, "Reminder", "Customer Order Due!", ToolTipIcon.None)
End Sub
End Class
You need to use a Timer and to avoid the constant reading of the database you may use something very simple as a DateTimePicker or a TextBox. In this example I'll use a Texbox.
So, insert a TextBox1 in your form and had the value of the first date/time (read from your database just once). The Textbox1 text should be something like "2021-10-21 15:00" (don't put the seconds and don't forget to have a space between date and time)
TextBox1.Text="2021-10-21 15:00" ' read this data from your database
Then you need to insert a Timer in your form and add this values:
Timer1.Interval = 60000 ' 1 minute
Timer1.Enabled = True
For the last you need to double click the Timer1 and write this code:
Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
If TextBox1.Text = Now.ToString("yyyy-MM-dd HH:mm") Then
' At this moment you should read the next date/time and put that in TextBox1
MsgBox("Show the popup notification")
End If
End Sub
Since you are using DB connections, ill assume you are familiar with multi threading.
RULE 1. The easiest way to do this is for your application to keep track of (in RAM) the next notification. Any time you add a new one through your app it will both insert it into the DB as well as check to see if it is going to elapse before the current one in RAM. When the one in RAM expires it grabs the next one from the DB. All that to say, you find a way to ensure that the next one to elapse is kept in RAM at any given time.
RULE 2. Then, when a notification is 'saved' in RAM, compare the time when the notification should be displayed - current time, this will give you how long until the notification should de shown and set a timer for that amount of time.
RULE 3. When the timer has elapsed display the notification for the one that is in RAM (Per rule 1 this is the one that just elapsed). And iterate the DB to find the next one to expire. Rinse and repeat. (Note, youll need to handle edge cases for when the first one is intered as there will not be any to compare to, and for when the last one is notified as there will not be any notifications left to store in RAM)
Tada, sorry i dont have code samples. But that is how i have solved this very issue numerous times. It works quite nicely.
EDIT 1: CODE
'main for testing
Dim notificationScheduler As NotificationScheduler = NotificationScheduler.getInstance
notificationScheduler.insertNotification(New MyNotificationEntity("Yay, working", DateTime.Now.AddSeconds(15)))
' expected that the one below will be displayed since insertNotification will only keep track of the newest one in the queue
' As mentioned in your post, you have a DB to persist them long term and as such will only need one in the APP's RAM at a time
notificationScheduler.insertNotification(New MyNotificationEntity("Yay, working-2", DateTime.Now.AddSeconds(3)))
' myNotificationEntity, will be replaced by whatever class you are using to package your reminder's in a single class (dont see on in your code sample)
Public Class MyNotificationEntity
Public data As String
Public timeToShow As DateTime
' this is a dumb bucket class, you will not end up using this one but rather whatever class you use to contain your notifications
' you will need to add _timeToShow as a variable to your class or change the code in the NotificationScheduler to be compatible with your Notification class
Public Sub New(data As String, timeToShow As DateTime)
Me.data = data
Me.timeToShow = timeToShow
End Sub
End Class
'NotificationScheduler class
Public Class NotificationScheduler
Private Shared _instance As NotificationScheduler
Private Shared ReadOnly _lock As Object = New Object()
Dim queuedNotification As MyNotificationEntity
Dim WithEvents timer As System.Windows.Forms.Timer = New Timer
Dim timerElapsedTime As DateTime = DateTime.Now
Private Sub New()
timer.Enabled = False
End Sub
Public Shared Function getInstance()
' enforce singleton design pattern (i.e. there should only be one of these in existence)
' If this does not make sense, that is fine, at some point lookup the 'singleton design pattern'
' Also, SyncLock is a VB (.net really) element that means in a multi-threaded application only one of them at any given time is inside the SyncLock block,
' the rest are queued to alleviate race conditions. Again, if this does not make sense, lookup 'race conditions'
SyncLock _lock
If (_instance Is Nothing) Then
_instance = New NotificationScheduler()
End If
End SyncLock
Return _instance
End Function
' called after you have placed the new notification in your DB, replaces the currently queued notification only if the new one will be shown first
Public Sub insertNotification(notificationEntity As MyNotificationEntity)
If timerElapsedTime.Ticks < notificationEntity.timeToShow.Ticks Then
queuedNotification = notificationEntity
timer.Stop()
Dim span As TimeSpan = notificationEntity.timeToShow - timerElapsedTime
timer.Interval = CInt(span.TotalMilliseconds)
timer.Start()
End If
End Sub
Private Sub timer_elapsed() Handles timer.Tick
' the notification time has arrived
showNotification()
readNextFromDb()
End Sub
Private Sub readNextFromDb()
' read the next (if any) from the and handle edge cases for when there is not next one in the DB
'IMPORTANT: if the one you read from the DB has already happened (i.e. the notification dateTime is after now, notify and do this method again)
'' your code here
End Sub
Private Sub showNotification()
MsgBox(queuedNotification.data) ' do whatever notification you want
End Sub
End Class
You will need to (it appears) create a class NotificationDAO (Data Access Object) or NotificationDTO (Data Transfer Object) or a generic NotificationEntity. That class will have a constructor and public instance variables for the items you need to store about a notification (i.e. every column you have in the 'TblReminder ' table) and everything should work fairly easily.
Note, the above code does not work nor check for pulling notifications out of a DB that have already passed their notification time, read the comments in the code for further information.
I have two forms, Form1 and Newform. Form1 has two buttons and a textbox and Newform has its own textbox. I am using a settext sub to invoke a delegate sub in the backgroundworker to update the textbox in both forms.
The textbox in Form1 seems to be updating but the textbox in Newform isn't updating.
Is there something that I'm missing if I want to update the textbox on a different form?
Thanks in advance.
Imports System.Threading
Public Class Form1
Dim stopbit As Boolean
Dim TestingComplete As Boolean
Dim ReadValue As Double
Dim FinalValue As Double
Delegate Sub SetTextCallback(ByRef Txtbox As TextBox, ByVal Txt As String)
'Thread Safe textbox update routine
Private Sub SetText(ByRef Txtbox As TextBox, ByVal Txt As String)
' InvokeRequired required compares the thread ID of the
' calling thread to the thread ID of the creating thread.
' If these threads are different, it returns true.
Console.WriteLine(Txtbox.InvokeRequired & " textbox invokerequired")
If Txtbox.InvokeRequired Then
Try
'MsgBox("inside settext")
Txtbox.Invoke(New SetTextCallback(AddressOf SetText), Txtbox, Txt)
Catch ex As Exception
MsgBox(ex.Message)
End Try
Else
Txtbox.Text = Txt
Txtbox.Update()
End If
End Sub
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
newform.Show()
End Sub
Function ReadTemp() As Double
ReadValue = ReadValue / 2
Return ReadValue
End Function
Sub Test()
Dim starttime As Integer
Dim EllapsedTime As Integer
Dim OldValue As Double = 0
Dim NewValue As Double = 0
Dim Difference As Double = 1
Dim Margin As Double = 0.1
stopbit = False
starttime = My.Computer.Clock.TickCount
Do
Thread.Sleep(200)
OldValue = NewValue
NewValue = ReadTemp()
Difference = Math.Abs(NewValue - OldValue)
SetText(Me.TextBox1, Difference.ToString)
SetText(newform.TextBox1, Difference.ToString)
newform.Refresh()
EllapsedTime = My.Computer.Clock.TickCount - starttime
Loop Until EllapsedTime > 5000 Or stopbit = True ' Or Difference < Margin
FinalValue = NewValue
TestingComplete = True
End Sub
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
BackgroundWorker1.RunWorkerAsync()
End Sub
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
stopbit = True
End Sub
Private Sub BackgroundWorker1_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
For i As Integer = 1 To 10
ReadValue = 100000
TestingComplete = False
ThreadPool.QueueUserWorkItem(AddressOf Test)
Do
Thread.Sleep(200)
Loop Until TestingComplete = True
MsgBox("Final Value " & FinalValue)
Next
End Sub
End Class
Your issue is due to that you're using the default instance of newform. In VB.NET default form instances is a feature that allows you to access a form via its type name without having to manually create an instance of it.
In other words it lets you do this:
newform.Show()
newform.TextBox1.Text = "Something"
...instead of doing it the correct way, which is this:
Dim myNewForm As New newform
myNewForm.Show()
myNewForm.TextBox1.Text = "Something"
Above we create a new instance of newform called myNewForm. This is required to be able to use most objects in the framework (including forms). However, VB.NET simplifies this behaviour by offering to create the instance for you, which is what is going on in my first example.
The problem with these default instances is that they are thread-specific, meaning a new instance is created for every thread that you use this behaviour in.
Thus the form you refer to when you do:
newform.Show()
...is not the same form that you refer to in your thread, because a new instance has been created for it in that thread:
'This is not the same "newform" as above!
SetText(newform.TextBox1, Difference.ToString)
The solution to this is of course to create the instance yourself, allowing you to have full control over what's going on:
Dim newFrm As New newform
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
newFrm.Show()
End Sub
...your code...
Sub Test()
...your code...
SetText(newFrm.TextBox1, Difference.ToString)
...even more of your code...
End Sub
As a side note you can remove your calls to newform.Refresh() and Txtbox.Update(). These just cause unnecessary overhead by forcing the form and text boxes to redraw themselves, which is already done when you change any of their properties that affect their contents/design (so you are essentially making them redraw themselves twice).
Also, if you want to make invoking to the UI thread simpler and you are using Visual Studio/Visual Basic 2010 or newer, you could switch to using lambda expressions instead of regular delegates. They're much easier to use and allows you to create whole methods in-line that can be invoked on the UI thread.
For this purpose I've written an extension method called InvokeIfRequired() which lets you invoke any method/function on the UI thread, checking InvokeRequired for you. It's similar to what you have now, only it works for any control (not just text boxes) and with lambda expressions, allows you to run any code you want on the UI.
You can use it by adding a module to your project (Add New Item... > Module) and naming it Extensions. Then put this code inside it:
Imports System.Runtime.CompilerServices
Public Module Extensions
''' <summary>
''' Invokes the specified method on the calling control's thread (if necessary, otherwise on the current thread).
''' </summary>
''' <param name="Control">The control which's thread to invoke the method at.</param>
''' <param name="Method">The method to invoke.</param>
''' <param name="Parameters">The parameters to pass to the method (optional).</param>
''' <remarks></remarks>
<Extension()> _
Public Function InvokeIfRequired(ByVal Control As Control, ByVal Method As [Delegate], ByVal ParamArray Parameters As Object()) As Object
If Parameters IsNot Nothing AndAlso _
Parameters.Length = 0 Then Parameters = Nothing
If Control.InvokeRequired = True Then
Return Control.Invoke(Method, Parameters)
Else
Return Method.DynamicInvoke(Parameters)
End If
End Function
End Module
This allows you to invoke either one line of code by doing:
Me.InvokeIfRequired(Sub() Me.TextBox1.Text = Difference.ToString())
Or to invoke a whole block of code by doing:
Me.InvokeIfRequired(Sub()
Me.TextBox1.Text = Difference.ToString()
newFrm.TextBox1.Text = Difference.ToString()
Me.BackColor = Color.Red 'Just an example of what you can do.
End Sub)
YourSubHere
Me.Invoke(Sub()
Form1.Textbox1.text="some text1"
Form2.Textbox2.text="some text2"
End Sub)
End Sub
Or if it's a one liner.
Me.Invoke(Sub() Form1.Textbox1.text="some text1")
Depending on what you need, you could invoke just some control like:
Textbox1.invoke(Sub() Textbox1.text="some text1")
I'm trying to make a "rotator" on a form that cycles through a series of urls and displays the url in the WebBrowser control. The following code displays my form, but the form remains white/blank and then the last url in the array appears after a while. When I put a MessageBox in-between each url, to create a stop, it works and each url appears. I've tried putting a Sleep in place of the MessageBox, but that didn't work. I've also tried increasing the Sleep time, but that didn't work either. How can I make it work correctly?
Sub Rotate()
Dim Urls() As String = {"www.stackoverflow.com", "www.google.com", "www.yahoo.com"}
Dim counter As Integer = 0
Form3.Show()
Do Until counter = 3
Form3.WebBrowser1.ScriptErrorsSuppressed = True
Form3.WebBrowser1.Navigate(Urls(counter))
'MessageBox.Show("Next")
counter = counter + 1
System.Threading.Thread.Sleep(2000)
Loop
End Sub
You can call Application.DoEvents after changing the URL so that the control gets the chance to redraw itself.
However, a better approach would be to use a timer which fires every 2 seconds and then change the URL in the event handler so that your UI keeps responsive.
For example setup a new field myTimer in your form, init it in your form's loading event and in the Tick event you call your Rotate method. As Rotate is now called several times, we have to move the counter variable out of the method and make it a field so that we keep its value between the invocations. I usually write C# so hopefully I did not make some typos below :)
Private WithEvents myTimer As System.Windows.Forms.Timer
Private counter As Integer
Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
' ... your existing code ...
counter = 0
myTimer = New System.Windows.Forms.Timer
myTimer.Interval = 2000
myTimer.Enabled = True
myTimer.Start()
End Sub
Private Sub myTimerTick() Handles myTimer.Tick
Rotate()
End Sub
Sub Rotate()
Dim Urls() As String = {"www.stackoverflow.com", "www.google.com", "www.yahoo.com"}
WebBrowser1.ScriptErrorsSuppressed = True
WebBrowser1.Navigate(Urls(counter))
counter = counter + 1
If counter > 3 Then myTimer.Stop()
End Sub
I am trying to create a timer that will countdown from the specified time.
The user enters a time and clicks a button.
The button click opens a second form that has a timer in it.
Every time the timer ticks, the time decreases and the time left is displayed in a textbox on form2 (textbox.text = timeLeft).
However, the textbox will never actually update. It remains blank, and the only time that assigning a new value to the .text property will actually work is if I raise an event (for example clicking a button that will change the .text property of the textbox)
*Here is the code for the timer class
Public Class CountdownTimer
Private timeAtStart As Integer
Private timeLeft As Integer
Public Sub StartTimer(ByVal time As Integer)
timeAtStart = time
timeLeft = timeAtStart
Timer1.Enabled = True
End Sub
Private Sub Timer1_Tick(ByVal sender As Object, ByVal e As System.EventArgs) Handles Timer1.Tick
If timeLeft > 0 Then
timeLeft = timeLeft - 1
txtTimeLeft.Text = timeLeft.ToString
Else
Timer1.Stop()
txtTimeRemaining.Text = "Time!"
txtTimeRemaining.ForeColor = Color.Red
End If
End Sub
End Class
And here is how I call it:
Dim timer As New CountdownTimer
timer.Show()
CountdownTimer.StartTimer(CInt(txtSetTime.Text))
Your code is calling the (form) class not the instance, and I cant see where Timer1 is properly referenced for an independant reusable class. Here is one way to implement a CountDown class that will work with other forms....
Friend Class CountdownTimer
Private timeAtStart As Integer
Private timeLeft As Integer
Private WithEvents Timer1 As New Timer
Private txtTimeLeft as TextBox
Public Sub New(TargetTB as TextBox)
txtTimeLeft= TargetTB
End Sub
Public Sub StartTimer(ByVal time As Integer, timeLength as Integer)
timeAtStart = time
timeLeft = timeLength
Timer1.Enabled = True
End Sub
Private Sub Timer1_Tick(ByVal sender As Object, ByVal e As System.EventArgs)_
Handles Timer1.Tick
' just dislaying time left
If timeLeft > 0 Then
timeLeft = timeLeft - 1
txtTimeLeft.Text = timeLeft.ToString
Else
Timer1.Stop()
txtTimeLeft.Text = "Time!"
txtTimeLeft.ForeColor = Color.Red
End If
End Sub
End Class
How to use it:
Dim CountDn As New CountdownTimer(frm.TextBoxToUse)
' use the INSTANCE name not the class name!!!!
'CountdownTimer.StartTimer(CInt(txtSetTime.Text))
CountDn.StartTimer(CInt(txtSetTime.Text))
If it displays the result after the timer has completed, i think you should use the
Application.DoEvents()
method to see the update immediately. It actually works with Windows Forms. What have you tried, so i can help further
You do realize that when you are counting down you are setting a different textbox than when it is complete, right?
txtTimeLeft.Text
VS
txtTimeRemaining.Text
Note: Timers run on the same thread as the UI so if you computer (or program) gets busy, the timer will NOT tick at exact intervals. If you are worried about small variances in your timer, you should compare the difference of your computer time during each tick event to determine how much time had passed.
Dim TS = TimeSpan = Now.Subtract(StartingTime)
Try refreshing the text boxes after each update:
So after
txtTimeLeft.Text = timeLeft.ToString
Add
txtTimeLeft.Refresh
This is your problem:
Dim timer As New CountdownTimer
timer.Show()
CountdownTimer.StartTimer(CInt(txtSetTime.Text))
You instantiate a new object called timer, but then start the timer on the CountdownTimer object
You need to change your code to this:
Dim timer As New CountdownTimer
timer.Show()
timer.StartTimer(CInt(txtSetTime.Text))
I have been pulling my hair out trying to get this to work. If I step through the code in debugger it all works great.
My problem is if I just run it, only the last task responds. I'm guessing I am overwriting the background working or something. I am sure I am doing a few things wrong but my code is now messy as I tried many way while searching. I know of the threadpool and .Net 4.0 tasks but having a hard time getting to do what I need.
Basicly I am writing a program (trying more likely) that takes a list of computers and pings then, then checks their uptime and reports back.
This works fine in the UI thread (Obviously that locks up my screen). I can have the background worker just do this, but then it does each computer 1 by one, and while the screen is responsive it still takes a long time.
So my answer was to have a for loop for each server launching a new background worker thread. My solution does not work.
I have seen other threads that I could do it, but I need to use with events to call code to update to UI when each is done.
What is the most simple way to do this?
Here is my code. Most is just copy paste + modify till I get it working right.
So In the main class I have the testworker.
(I tried using Testworker() but it said I could not do that WithEvents)
When I click the button the list loads.
Private WithEvents TestWorker As System.ComponentModel.BackgroundWorker
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles Button1.Click
Button1.IsEnabled = False
Dim indexMax As Integer
indexMax = DataGridStatus.Items.Count
For index = 1 To (indexMax)
Dim Temp As ServerInfo = DataGridStatus.Items(index - 1)
Temp.Index = index - 1
Call_Thread(Temp)
Next
End Sub
Private Sub Call_Thread(ByVal server As ServerInfo)
Dim localserver As ServerInfo = server
TestWorker = New System.ComponentModel.BackgroundWorker
TestWorker.WorkerReportsProgress = True
TestWorker.WorkerSupportsCancellation = True
TestWorker.RunWorkerAsync(localserver)
End Sub
Private Sub TestWorker_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles TestWorker.DoWork
Dim iparray As IPHostEntry
Dim ip() As IPAddress
Dim Server As ServerInfo
Server = e.Argument
Try
'Get IP Address first
iparray = Dns.GetHostEntry(Server.ServerName)
ip = iparray.AddressList
Server.IPAddress = ip(0).ToString
'Try Pinging
Server.PingResult = PingHost(Server.ServerName)
If Server.PingResult = "Success" Then
'If ping success, get uptime
Server.UpTime = GetUptime(Server.ServerName)
Else
Server.PingResult = "Failed"
End If
Catch ex As Exception
Server.PingResult = "Error"
End Try
TestWorker.ReportProgress(0, Server)
Thread.Sleep(1000)
End Sub
Private Sub TestWorker_ProgressChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles TestWorker.ProgressChanged
Dim index As Integer
Dim serverchange As ServerInfo = DirectCast(e.UserState, ServerInfo)
index = DataGridStatus.Items.IndexOf(serverchange)
' index = serverchange.Index
DataGridStatus.Items.Item(index) = serverchange
' ProgressBar1.Value = e.ProgressPercentage
DataGridStatus.Items.Refresh()
End Sub
You are only getting the last result because you are blowing away your BackgroundWorker each time you call TestWorker = New System.ComponentModel.BackgroundWorker. Since the processing is being done asynchronously, this line is being called multiple times within your for loop before the previous work has finished.
Something like the following might work. (Sorry, my VB is rusty; there are probably more efficient ways of expressing this.)
Delegate Function PingDelegate(ByVal server As String) As String
Private _completedCount As Int32
Private ReadOnly _lockObject As New System.Object
Dim _rnd As New Random
Private _servers As List(Of String)
Private Sub GoButton_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles GoButton.Click
_servers = New List(Of System.String)(New String() {"adam", "betty", "clyde", "danny", "evan", "fred", "gertrude", "hank", "ice-t", "joshua"})
_completedCount = 0
ListBox1.Items.Clear()
GoButton.Enabled = False
BackgroundWorker1.RunWorkerAsync(_servers)
End Sub
Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
Dim servers As List(Of System.String) = DirectCast(e.Argument, List(Of System.String))
Dim waitHandles As New List(Of WaitHandle)
For Each server As System.String In servers
' Get a delegate for the ping operation. .Net will let you call it asynchronously
Dim d As New PingDelegate(AddressOf Ping)
' Start the ping operation async. When the ping is complete, it will automatically call PingIsDone
Dim ar As IAsyncResult = d.BeginInvoke(server, AddressOf PingIsDone, d)
' Add the IAsyncResult for this invocation to our collection.
waitHandles.Add(ar.AsyncWaitHandle)
Next
' Wait until everything is done. This will not block the UI thread because it is happening
' in the background. You could also use the overload that takes a timeout value and
' check to see if the user has requested cancellation, for example. Once all operations
' are complete, this method will exit scope and the BackgroundWorker1_RunWorkerCompleted
' will be called.
WaitHandle.WaitAll(waitHandles.ToArray())
End Sub
Private Sub BackgroundWorker1_ProgressChanged(ByVal sender As System.Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
ListBox1.Items.Add(String.Format("{0} ({1}% done)", e.UserState, e.ProgressPercentage))
End Sub
Private Sub BackgroundWorker1_RunWorkerCompleted(ByVal sender As System.Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted
GoButton.Enabled = True
End Sub
Private Function Ping(ByVal server As System.String) As System.String
' Simulate a ping with random result and duration
Threading.Thread.Sleep(_rnd.Next(1000, 4000))
Dim result As Int32 = _rnd.Next(0, 2)
If result = 0 Then
Return server & " is ok"
Else
Return server & " is down"
End If
End Function
Private Sub PingIsDone(ByVal ar As IAsyncResult)
' This method is called everytime a ping operation completes. Note that the order in which
' this method fires is completely independant of the order of the servers. The first server
' to respond calls this method first, etc. This keeps optimal performance.
Dim d As PingDelegate = DirectCast(ar.AsyncState, PingDelegate)
' Complete the operation and get the result.
Dim pingResult As String = d.EndInvoke(ar)
' To be safe, we put a lock around this so that _completedCount gets incremented atomically
' with the progress report. This may or may not be necessary in your application.
SyncLock (_lockObject)
_completedCount = _completedCount + 1
Dim percent As Int32 = _completedCount * 100 / _servers.Count
BackgroundWorker1.ReportProgress(percent, pingResult)
End SyncLock
End Sub
Update: I posted this answer focusing on exactly what you were trying to do from a technical standpoint (use many background workers) without really putting much thought into whether or not this was a good way to accomplish your real objective. In fact, I think you could achieve what you're going for much more easily with a single BackgroundWorker and something like a Parallel.ForEach loop in its DoWork event handler (this takes care of a lot of the nitty gritty work in, e.g., Dave's solution).
When you declare WithEvents TestWorker As BackgroundWorker in VB it wraps it up something like this (not exactly—this is just to illustrate the idea):
Private _TestWorker As BackgroundWorker
Private Property TestWorker As BackgroundWorker
Get
Return _TestWorker
End Get
Set(ByVal value As BackgroundWorker)
' This is all probably handled in a more thread-safe way, mind you. '
Dim prevWorker As BackgroundWorker = _TestWorker
If prevWorker IsNot Nothing Then
RemoveHandler prevWorker.DoWork, AddressOf TestWorker_DoWork
' etc. '
End If
If value IsNot Nothing Then
AddHandler value.DoWork, AddressOf TestWorker_DoWork
' etc. '
End If
_TestWorker = value
End Set
End Property
When you realize this, it becomes clear that by setting TestWorker to a new BackgroundWorker on every call to Call_Thread, you are removing any attached handlers from the object previously referenced by the field.
The most obvious fix would simply be to create a new local BackgroundWorker object in each call to Call_Thread, attach the handlers there (using AddHandler and RemoveHandler), and then just let it do its thing:
Private Sub Call_Thread(ByVal server As ServerInfo)
Dim localserver As ServerInfo = server
' Use a local variable for the new worker. '
' This takes the place of the Private WithEvents field. '
Dim worker As New System.ComponentModel.BackgroundWorker
' Set it up. '
With worker
.WorkerReportsProgress = True
.WorkerSupportsCancellation = True
End With
' Attach the handlers. '
AddHandler worker.DoWork, AddressOf TestWorker_DoWork
AddHandler worker.ProgressChanged, AdressOf TestWorker_ProgressChanged
' Do the work. '
worker.RunWorkerAsync(localserver)
End Sub
Creating the worker right there in the method should be fine as long as you do so from the UI thread, since BackgroundWorker automatically attaches to the current SynchronizationContext in its constructor (if I remember correctly).
Ideally you should use only 1 backgroundworker and use it like this:
Assemble all the work that needs to be done: in your case a list of ServerInfo
Do the work in the background: ping all the servers and keep the result
Report progress: for example after each server pinged
Put results back in DoWorkEventArgs.Result
Display the results back in your UI.
You need to attach TestWorker_DoWork and TestWorker_ProgressChanged to the DoWork and ProgressChanged events within Call_Thread. I haven't yet examined the rest of the code, but that is why it isn't doing anything now.
TestWorker = New System.ComponentModel.BackgroundWorker
TestWorker.WorkerReportsProgress = True
TestWorker.WorkerSupportsCancellation = True
AddHandler TestWorker.DoWork, AddressOf TestWorker_DoWork
AddHandler TestWorker.ProgressChanged, AddressOf TestWorker_ProgressChanged
TestWorker.RunWorkerAsync(localserver)