I am working on serialport that can send and received certain commands. I would like to implement a retry feature which will allow me (the client) to resend data until the device (server) received and send a response to me.
Because of that I created a simple code that can illustrate this kind of function.
Public Class Form1
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
RichTextBox1.AppendText(Environment.NewLine & "Sample")
End Sub
Private Sub RichTextBox1_TextChanged(sender As Object, e As EventArgs) Handles RichTextBox1.TextChanged
Console.WriteLine("Trigger textchanged")
Dim totalLines As Integer = Me.RichTextBox1.Lines.Length
Dim lastLine As String = Me.RichTextBox1.Lines(totalLines - 1)
Dim CSTAT_Check As Boolean = lastLine Like "*Sample*"
If CSTAT_Check = True Then
RichTextBox1.AppendText(Environment.NewLine & "Sample")
End If
End Sub
End Class
The way it works is like this, I will clicked the button to append a sample string to richtextbox then the richtextbox textchange_event will be triggered causing it to resend the sample string to itself and will causes it to trigger another textchange_event and so on and so forth until the device received the sample string which in return the device (server) will send a sample_accepted string to my device (client) and because the textchanged_event doesnt detect the sample string in the last line of richtextbox it will no longer send another sample string to richtextbox.
It's little hard to understand so I will create a simple diagram
Client (Me) Server (Device)
Send sample string Doesn't detected
Send sample string again Doesn't detected again
Send sample string again Doesn't detected again
Send sample string again Doesn't detected again
Send sample string again Detected sample will send sample_accepted
Client Will no longer send sample string because the server detected it already.
The problem in my code is it seems like it doesn't trigger the textchanged_event again after its first trigger.
???If you change a property inside the code that responds to that property being changed, another changed event will not fire.??? You need to manually trigger the textchanged event after you make the changes.
Public Class Form1
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
RichTextBox1.AppendText(Environment.NewLine & "Sample")
End Sub
Private Sub RichTextBox1_TextChanged(sender As Object, e As EventArgs) Handles RichTextBox1.TextChanged
Console.WriteLine("Trigger textchanged")
Dim totalLines As Integer = Me.RichTextBox1.Lines.Length
Dim lastLine As String = Me.RichTextBox1.Lines(totalLines - 1)
Dim CSTAT_Check As Boolean = lastLine Like "*Sample*"
If CSTAT_Check = True Then
RichTextBox1.AppendText(Environment.NewLine & "Sample")
RichTextBox1_TextChanged(sender, New EventArgs())
End If
End Sub
End Class
Related
I'm new to this site and also a newbee in vb.net, I created a simple form in vb.net, a form with 3 buttons, by clicking Button1 Species1.txt is created, and by clicking Button2 the lines in Species1.txt are copied in a String Array called astSpecies(), and by Button3 the String Array is copied in a new file, named Species2.txt, below is the code.
Public Class Form4
Dim astSpecies() As String
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim myStreamWriter = New StreamWriter("C:\Users\Administrator\Documents\species1.txt", True)
myStreamWriter.WriteLine("Pagasius pangasius")
myStreamWriter.WriteLine("Meretrix lyrata")
myStreamWriter.WriteLine("Psetta maxima")
myStreamWriter.WriteLine("Nephrops norvegicus")
myStreamWriter.WriteLine("Homarus americanus")
myStreamWriter.WriteLine("Procambarus clarkii")
myStreamWriter.Close()
MsgBox("list complete")
End Sub
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
Dim myStreamReader = New StreamReader("C:\Users\Administrator\Documents\species1.txt")
Dim i As Integer
Dim stOutput As String
stOutput = ""
Do While Not myStreamReader.EndOfStream
astSpecies(i) = myStreamReader.ReadLine
stOutput = stOutput & astSpecies(i) & vbNewLine
i = i + 1
Loop
myStreamReader.Close()
MsgBox(stOutput)
End Sub
Private Sub Button3_Click(sender As Object, e As EventArgs) Handles Button3.Click
Dim myStreamWriter = New StreamWriter("C:\Users\Administrator\Documents\species2.txt", True)
Dim o As Integer
Do While o <= astSpecies.Length
myStreamWriter.WriteLine(astSpecies(o))
o = o + 1
Loop
myStreamWriter.Close()
End Sub
End Class
First of all, you should make a few settings when it comes to VB.Net. 1.) set Option Strict to On 2.) remove the VB6 namespace. VB6 is the old Visual Basic. There are many functions in this that are inefficient from today's perspective. So please do not write MsgBox() but MessageBox.Show("").
(If you still need control characters such as NewLine or Tab, you can set a selective reference with Imports Microsoft.VisualBasic.ControlChars. Sounds contradictory, but it is useful, because why should you also write ChrW(9), it is not legible.)
I quickly started a project myself and wrote whatever you wanted.
I still don't quite understand why you first write things into a text file, then read them out, and then write that into a second text file – I want to say: where do the strings originally come from? The strings must have been there already? Anyway, I filled a List(of String) in the Button2_Click procedure. This has the advantage that you don't have to know in advance how many strings are coming, and you can sort them later and so on ...
You should also discard all Writers when you no longer need them. So use Using. Otherwise it can happen that the written files are not discarded and you can no longer edit the file.
Imports Microsoft.VisualBasic.ControlChars
Imports Microsoft.WindowsAPICodePack.Dialogs
Public NotInheritable Class FormMain
Private Path As String = ""
Private allLines As New List(Of String)
Private Sub FormMain_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Me.BackColor = Color.FromArgb(161, 181, 165)
End Sub
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Using OFolderD As New CommonOpenFileDialog
OFolderD.Title = "Ordner auswählen"
OFolderD.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop)
OFolderD.IsFolderPicker = True
If OFolderD.ShowDialog() = CommonFileDialogResult.Ok Then
Path = OFolderD.FileName
Else
Return
End If
End Using
Path &= "\Data.txt"
Using txtfile As System.IO.StreamWriter = My.Computer.FileSystem.OpenTextFileWriter(Path, True)
txtfile.WriteLine("Pagasius pangasius")
txtfile.WriteLine("Meretrix lyrata")
txtfile.WriteLine("Psetta maxima")
txtfile.WriteLine("Nephrops norvegicus")
txtfile.WriteLine("Homarus americanus")
txtfile.WriteLine("Procambarus clarkii")
txtfile.Close()
End Using
End Sub
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
'read all Text
Dim RAT() As String = System.IO.File.ReadAllLines(Path, System.Text.Encoding.UTF8)
If RAT.Length = 0 OrElse RAT.Length = 1 Then
MessageBox.Show("The File only contains 0 or 1 characters.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Hand)
Return
End If
allLines.AddRange(RAT)
End Sub
Private Sub Button3_Click(sender As Object, e As EventArgs) Handles Button3.Click
Dim Pfad_txt As String = Path.Substring(0, Path.LastIndexOf("\"c)) & "\Data2.txt"
Using txtfile As System.IO.StreamWriter = My.Computer.FileSystem.OpenTextFileWriter(Pfad_txt, True)
For Each Line As String In allLines
txtfile.WriteLine(Line)
Next
txtfile.Close()
End Using
End Sub
End Class
By the way: I use a FolderBrowserDialog in the Button1_Click procedure. This should be done so that the program also runs properly on other PCs. In order to be able to use the FBD, you have to download Microsoft.WindowsAPICodePack.Dialogs in Visual Studio's own Nuget package manager.
how to set Option Strict to On
How to uncheck VB6.
how to install FolderBrowserDialog in Visual Studio
Button1
If you want to use a StreamWriter it should be disposed. Classes in the .net Framework that have a Dispose method may use resources outside of the framework which need to be cleaned up. The classes shield you from these details by provided a Dispose method which must be called to properly do the clean up. Normally this is done with Using blocks.
I used a string builder which saves creating and throwing away a string each time you change the string. You may have heard that strings are immutable (cannot be changed). The StringBuilder class gets around this limitation. It is worth using if you have many changes to your string.
The File class is a .net class that you can use to read or write files. It is not as flexible as the stream classes but it is very easy to use.
Button 2
When you declared your Array, you declared an array with no elements. You cannot add elements to an array with no space for them. As Daniel pointed out, you can use the .net class List(Of T) The T stands for Type. This is very good suggestion when you don't know the number of elements in advance. I stuck with the array idea by assigning the array returned by File.ReadAllLines to the lines variable.
You get the same result by simply reading all the text and displaying it.
Button 3
Again I used the File class here which allows you to complete your task in a single line of code. Using 2 parameters for the String.Join method, the separator string and the array to join, we reproduce the original file.
Private SpeciesPath As String = "C:\Users\maryo\Documents\species1.txt"
Private lines As String()
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim sb As New StringBuilder
sb.AppendLine("Pagasius pangasius")
sb.AppendLine("Meretrix lyrata")
sb.AppendLine("Psetta maxima")
sb.AppendLine("Nephrops norvegicus")
sb.AppendLine("Homarus americanus")
sb.AppendLine("Procambarus clarkii")
File.WriteAllText(SpeciesPath, sb.ToString)
MsgBox("list complete")
End Sub
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
lines = File.ReadAllLines(SpeciesPath)
MessageBox.Show(String.Join(Environment.NewLine, lines))
'OR
MessageBox.Show(File.ReadAllText(SpeciesPath))
End Sub
Private Sub Button3_Click(sender As Object, e As EventArgs) Handles Button3.Click
File.WriteAllLines("C:\Users\maryo\Documents\species2.txt", lines))
End Sub
I created a simple hello world program to download data from the web.
Private Sub cmdSurf_Click(sender As Object, e As EventArgs) Handles cmdSurf.Click
Dim wb As New System.Net.WebClient
Dim uri1 = New Uri(TextBox1.Text)
Dim str = wb.DownloadString(uri1)
TextBox2.Text = str
End Sub
It's pretty simple right. I use a WebClient object to download a string syncrhonously and then I display the result in a TextBox.
Now, I want to do so asynchronously.
Basically, after I download the URI I do something else.
Then after the download is finished, I am doing what it should be.
So I did
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
Dim wb As New System.Net.WebClient
Dim uri1 = New Uri(TextBox1.Text)
wb.DownloadStringAsync(uri1)
TextBox2.Text = ""
End Sub
It turns out DownloadStringAsync(uri1) is a Sub, so it doesn't return anything.
So, well, what should be displayed in TextBox2 then? What am I missing?
Update: I realized that I should have used DownloadStringAsyncTask().
So I did this:
Private Async Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
Await downloadStringAndAssignText()
TextBox2.Text = "I am downloading this. This text will change once the download is finished"
End Sub
Private Async Function downloadStringAndAssignText() As Task
Dim wb As New System.Net.WebClient
Dim uri1 = New Uri(TextBox1.Text)
Dim str = Await wb.DownloadStringTaskAsync(uri1)
TextBox2.Text = str
End Function
This is almost correct.
The thing is I want to do this properly so that
TextBox2.Text = "I am downloading this. This text will change once..."
is called BEFORE wb.DownloadStringTaskAsync(uri1) is finished.
Also I do not want warning. So how exactly should I do that?
You can set the text of your TextBox before you call the async method that downloads the string. When Await DownloadStringAndAssignText() is called, control will return to the calling Thread (the UI Thread), so the TextBox.Text is set and shown.
When the async method returns, you can assign to the same TextBox the returned string value.
Your method should return a value, the string it downloaded, not assign a value to a UI element that doesn't belong to this method (its responsibility is to download a string): here the return type is set to Task(Of String).
You can directly assign the return value of the async method to the same TextBox.Text property.
The WebClient object should be declared with a Using statement, so it's disposed of when the method completes.
You can directly return the result of WebClient.DownloadStringTaskAsync(), since this method returns a string, the same return type of your method.
Imports System.Net
Private Async Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
TextBox2.Text = "I am downloading..."
TextBox2.Text = Await DownloadStringAndAssignText(TextBox1.Text)
End Sub
Private Async Function DownloadStringAndAssignText(url As String) As Task(Of String)
Using wb As New WebClient()
Return Await wb.DownloadStringTaskAsync(New Uri(url))
End Using
End Function
So basically I am asking how to check if the URL of the webbrowser has changed (is diferrent from the previous one).
Thank you.
You can check ...
Sub webbrowser1_Complete(ByVal sender As Object, _
ByVal e As WebBrowserDocumentCompletedEventArgs) Handles WebBrowser1.DocumentCompleted
//Webbrowser1.url property to get valu of url
End Sub
Ok from what I can understand from your question this is what I would do. First create a structurethis will allow you to store data that you may want to use again. Next create a Function in this case with a Boolean return that checks to see if WebBrowser1's current url is the same as the one we have stored within our structure.And once you have done that, I would create a new WebBrowser1 Event in this case WebBrowser1_DocumentCompleted to trigger the Function to compare both the WebBrowser1 url textbox and the structures stored string when a web page as completely loaded.
Public Class Form1
Dim urlSettings As urlSetting
Structure urlSetting
Public url As String
End Structure
Private Function checkURL(url As String) As Boolean
Dim changed As Boolean = True
If Not urlSettings.url = url Then
changed = False
End If
Return changed
End Function
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
WebBrowser1.Navigate(New Uri(TextBox1.Text))
End Sub
Private Sub WebBrowser1_DocumentCompleted(sender As Object, e As WebBrowserDocumentCompletedEventArgs) Handles WebBrowser1.DocumentCompleted
If Not checkURL(TextBox1.Text) Then
urlSettings.url = TextBox1.Text
MessageBox.Show("The URL has changed")
End If
End Sub
End Class
Of course you can modify so suit your needs, however this should get you on your way. :) MSDN Information Structure: https://msdn.microsoft.com/en-us/library/4ft0z102.aspx WebBrowser Control : https://msdn.microsoft.com/en-us/library/system.windows.forms.webbrowser(v=vs.110).aspx Return Statesments eg Function: https://msdn.microsoft.com/en-us/library/2e34641s.aspx
Please can someone tell me why my server locks up when my rclient connects and try to send data?
If I comment out:
Dim bytes(rclient.ReceiveBufferSize) As Byte
rstream.Read(bytes, 0, CInt(rclient.ReceiveBufferSize))
RString = Encoding.ASCII.GetString(bytes)
TextBox2.Text = RStringcode here
and just have
TextBox2.Text = ("Remote connected")
the server picks up on a connected client so it must be something i have done with the .read ?
Imports System.Net.Sockets
Imports System.Text
Imports System.Threading
Imports System.IO.Ports
Dim tclient As New TcpClient
Dim tstream As NetworkStream
Dim rclient As New TcpClient
Dim rstream As NetworkStream
Dim sArray() As String
Dim Tricopter As New TcpListener(2000)
Dim Remote As New TcpListener(2001)
Dim myVal As String
Dim TricopterThread As New Thread(AddressOf TricopterThreadSub)
Dim RemoteThread As New Thread(AddressOf RemoteThreadSub)
Dim tre(tclient.ReceiveBufferSize) As Byte
Dim TState = False
Dim RState = False
Dim SerialSwitch = False
Dim Toggle = False
Dim TString As String
Dim RString As String
Dim Remo(rclient.ReceiveBufferSize) As Byte
Dim TSendText() As Byte
Private Sub RemoteThreadSub()
rclient = Remote.AcceptTcpClient
rstream = rclient.GetStream
RState = True
End Sub
Private Sub TricopterThreadSub()
tclient = Tricopter.AcceptTcpClient
tstream = tclient.GetStream
TState = True
End Sub
Private Sub Form1_FormClosing(sender As Object, e As FormClosingEventArgs) Handles Me.FormClosing
End Sub
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
CheckForIllegalCrossThreadCalls = False
Tricopter.Start()
Remote.Start()
Timer1.Start()
TricopterThread.Start()
RemoteThread.Start()
End Sub
Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
myVal = ">" & TrackBar1.Value & "," & TrackBar2.Value & "," & TrackBar3.Value & "," & TrackBar4.Value & "/n"
Try
If TState = False Then
TextBox1.Text = ("No tricopter connected")
Else
TextBox1.Text = myVal
If Toggle = False Then
TSendText = Encoding.ASCII.GetBytes(myVal)
End If
If Toggle = True Then
TSendText = Encoding.ASCII.GetBytes(RString)
End If
tstream.Write(TSendText, 0, TSendText.Length)
End If
If RState = False Then
TextBox2.Text = ("No Remote connected")
Else
' TextBox2.Text = ("Remote connected")
Dim bytes(rclient.ReceiveBufferSize) As Byte
rstream.Read(bytes, 0, CInt(rclient.ReceiveBufferSize))
RString = Encoding.ASCII.GetString(bytes)
TextBox2.Text = RString
End If
Catch ex As Exception
End Try
End Sub
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
If Toggle = False Then
CheckBox1.CheckState = 1
Toggle = True
Else
CheckBox1.CheckState = 0
Toggle = False
End If
End Sub
End Class
You are assuming that reads return a specific amount of data such as ReceiveBufferSize. That is not true. A read returns at least one byte, that is all.
Just to clarify, TCP does not support message based transfer.
The correct way to read depends on the protocol. If the exact number of bytes expected is known you need to read in a loop until so many bytes are received (or use BinaryReader which does that for you).
For a line based protocol you can use StreamReader.ReadLine which again automates the looping.
ReceiveBufferSize is completely unrelated to how much data is available or will come.
DataAvailable is how much data can be read right now without blocking. But more data might come. It is almost always a bug to use it. Might return 0 at any time even if data comes in 1ms later.
EDIT: For those of you who have downloaded/tested this already, I made a bugfix to the classes so you'll need to redownload the sources if you're going to use them again.
If you want to perform proper data transfer you'll need to use a more reliable method than just simply reading random data. And as usr pointed out: the TcpClient.ReceiveBufferSize property does not tell you how much data there is to receive, nor how much data there is sent to you. ReceiveBufferSize is just a variable indicating how many bytes you are expected to get each time you read incoming data. Read the MSDN page about the subject for more info.
As for the data transfer, I've created two classes which will do length-prefixed data transfer for you. Just import them to your project and you'll be able to start using them immediatelly. Link: http://www.mydoomsite.com/sourcecodes/ExtendedTcpClient.zip
Example usage
Server side
First declare a new variable for ExtendedTcpClient, and be sure to
include WithEvents in the declaration.
Dim WithEvents Client As ExtendedTcpClient
Then you just need to use a normal TcpListener to check for
incoming connections. The TcpListener.Pending() method can be
checked in for example a timer.
When you are to accept a new TcpClient, first declare a new
instance of the ExtendedTcpClient. The class requires to have a
form as it's owner, in this application Me is the current form.
Then, use the ExtendedTcpClient.SetNewClient() method with
Listener.AcceptTcpClient() as it's argument to apply the
TcpClient from the listener.
If Listener.Pending() = True Then
Client = New ExtendedTcpClient(Me)
Client.SetNewClient(Listener.AcceptTcpClient())
End If
After that you won't be needing the timer anymore, as the
ExtendedTcpClient has it's own thread to check for data.
Now you need to subscribe to the PacketReceived event of the
client. Create a sub like so:
Private Sub Client_PacketReceived(sender As Object, e As ExtendedTcpClient.PacketReceivedEventArgs) Handles Client.PacketReceived
End Sub
In there you can for example output the received packet as text into
a TextBox. Just check if the packet header is PlainText and then
you can convert the received packets contents (which is an array of
bytes, accessed via e.Packet.Contents) to a string and put it in
the TextBox.
If e.Packet.Header = TcpMessagePacket.PacketHeader.PainText Then
TextBox1.AppendText("Message recieved: " & System.Text.Encoding.Default.GetString(e.Packet.Contents) & Environment.NewLine)
End If
Lastly, when closing the form you just need to disconnect the client.
Private Sub ServerWindow_FormClosing(sender As Object, e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
If Client IsNot Nothing Then Client.Disconnect()
End Sub
And that's it for the server side.
Client side
For the client side you will only be needing a normal TcpClient (unless you don't want to receive data there too).
Dim Client As New TcpClient
Then connect to the server via the IP and port you've given the listener.
Client.Connect("127.0.0.1", 5555) 'Connects to localhost (your computer) at port 5555.
Now if you want to send plain text to the server you'd do something like this:
Dim MessagePacket As New TcpMessagePacket(System.Text.Encoding.Default.GetBytes(TextBox2.Text), TcpMessagePacket.PacketHeader.PainText)
MessagePacket.Send(Client) 'Client is the regular TcpClient.
And now everything should be working!
Link to a complete example project: http://www.mydoomsite.com/sourcecodes/TCP%20Messaging%20System.zip
If you want to add more headers to the class, just open TcpMessagePacket.vb and add more values in the PacketHeader enum (located in the region called Constants).
Hope this helps!
Screenshot from the example project
(Click the image for larger resolution)
Part of a program I am modifying involves communicating through a serial port using a proprietary library. Unfortunately, this library does not have the same SerialPort.DataReceived event that the System.IO.Ports namespace contains. In fact, it has no events whatsoever, however it does have two functions that can probably be used similarly:
Port.WaitForData(int time)
This function waits the given amount of time to recieve some previously specified strings over the port. It returns 1 for yes, received string, or 0 for no, did not recieve string, timed out.
Port.IsReceiveBufferEmpty()
This function returns a boolean of yes, the receive buffer is empty or no, the receive buffer contains data.
It seems to me I will have to create some thread to be continuously looping whenever the port is opened and do one of these two things:
For every loop, call WaitForData(some big number) with the specified strings it is looking for set to "", or vbCrLf, or something else that I can confirm it will recieve everytime data is sent. If it finds smoething, read it and write to a textbox. If WaitForData doesn't find anything, loop again.
For every loop, call IsReceiveBufferEmpty(), and if it isn't, read it and write to a textbox.
What the best way to go about implementing this? The first options seems potentially more efficient to me, although I know next to nothing about how these method work under the hood. Obviously I want to keep my form responsive when doing this, so how should I go about continuously looping without freezing the form but being able to read any incoming data?
Thanks for your help.
Perhaps not the most elegant solution, but you could use a BackgroundWorker to do the IO. e.g. something like this pseudo-code:
Public Class MyForm
Private _port As ProprietaryIOLibrary
Private WithEvents Worker As System.ComponentModel.BackgroundWorker
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles Me.Load
_port = New ProprietaryIOLibrary()
Worker = New System.ComponentModel.BackgroundWorker()
Worker.WorkerReportsProgress = True
Worker.WorkerSupportsCancellation = True
Worker.RunWorkerAsync()
End Sub
Private Sub ButtonCancel_Click(sender As Object, e As EventArgs) Handles ButtonCancel.Click
Worker.CancelAsync()
End Sub
Private Sub Worker_DoWork(sender As Object, e As DoWorkEventArgs) Handles Worker.DoWork
Do
If _port.WaitForData(1000) Then ' Adjust timeout depending on cancel responsiveness?
Dim data As String = _port.ReadDataAsString() ' ?
' Trigger the ProgressChanged event, passing the data
Worker.ReportProgress(0, data)
End If
If Worker.CancellationPending Then
Exit Do
End If
Loop
End Sub
Private Sub Worker_ProgressChanged(sender As Object, e As ProgressChangedEventArgs) Handles Worker.ProgressChanged
' Update the UI with the data received
' ProgressChanged is called on the UI thread, so no need to Invoke
Dim data As String = DirectCast(e.UserState, String)
TextBox1.Text &= data & vbCrLf
End Sub
Private Sub Worker_RunWorkerCompleted(sender As Object, e As RunWorkerCompletedEventArgs) Handles Worker.RunWorkerCompleted
TextBox1.Text &= "Complete!"
End Sub
End Class