I have a RichTextBox on a Windows Form that I wish to update with 'checkpoints' as I go through an XML building process.
I have a Class called 'LogFiles' that has the Delegate for the RichTextBox
Public Delegate Sub AppendRichTextBoxDelegate(ByVal RTB As RichTextBox, ByVal txt As String)
In that Class I have the Sub to Append the RTB
Public Shared Sub AppendRichTextBox(ByVal RTB As RichTextBox, ByVal txt As String)
Try
If RTB.InvokeRequired Then
RTB.Invoke(New AppendRichTextBoxDelegate(AddressOf AppendRichTextBox), New Object() {RTB, txt})
Else
RTB.AppendText(txt & Environment.NewLine)
End If
Catch ex As Exception
MsgBox(MsgBox(ex.Message & Environment.NewLine & Environment.NewLine & ex.StackTrace))
End Try
End Sub
Now when I call my BackGroundWorker
Public Sub BGW1ProcessFile_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BGW1ProcessFile.DoWork
LogFiles.AppendRichTextBox(LogFileRTB, "Starting")
LogFiles.CreateLog()
Interchange.StartXML()
End Sub
That AppendRichTextBox works as expected and updates the RTB in real time, HOWEVER when I then stop into 'Interchange.StartXML' that also contains an AppendRichTextBox it does not update the RTB from within that Sub.
When the BackGroundWorker finishes the AppendRichTextBox for for the RunWorkerCompleted Event works as expected.
Why does the AppendRichTextBox Sub I have created with the delegate work on the BackGroundWorker Process but does not work on the Sub that is launched as part of the BackGroundWorker Process?
Its pickling my head a little bit, any assistance would be appreciated.
Regards,
James
As explained via comments, the BackgroundWorker is precisely meant to deal with the 2-thread situations involving GUI + another thread. That's why its in-built functionalities can gracefully account for the most likely situations.
In your case (i.e., regularly updating GUI elements from the BGW thread), you should rely on the ProgressChanged event. Here you have a sample code clearly showing how to use it:
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
BGW1ProcessFile.WorkerReportsProgress = True
BGW1ProcessFile.RunWorkerAsync()
End Sub
Private Sub BGW1ProcessFile_ProgressChanged(sender As Object, e As System.ComponentModel.ProgressChangedEventArgs) Handles BGW1ProcessFile.ProgressChanged
'You can include here as complex modifications on the GUI as
'required. Just store anything in e.UserState, as shown below
RTB.Text = RTB.Text & e.UserState.ToString & Environment.NewLine
End Sub
Private Sub modifyRTB(caseNo As Integer)
'Here you are calling the aforementioned ProgressChanged event.
BGW1ProcessFile.ReportProgress(0, "This is case " & caseNo.ToString)
End Sub
Private Sub BGW1ProcessFile_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BGW1ProcessFile.DoWork
BGW1ProcessFile.ReportProgress(0, "We are in the BGW thread now")
modifyRTB(1)
modifyRTB(2)
modifyRTB(3)
End Sub
Private Sub BGW1ProcessFile_RunWorkerCompleted(sender As Object, e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BGW1ProcessFile.RunWorkerCompleted
RTB.Text = RTB.Text & Environment.NewLine & "We are outside the BWG thread now"
End Sub
Related
Hi so i have small form application
the problem was that when i was running tasks on the main thread, the program window wasn't responding
so my button redirects the task to the backgroudworker:
Private Sub btn_start_Click(sender As Object, e As EventArgs) Handles btn_start.Click
BackgroundWorker1.RunWorkerAsync()
End Sub
Now i want to output to my textbox (txtlog) some text from this Backgroudworker multiple time in one Sub. I found a solution that allows this. but this makes the code quite ugly if I have to use it several times:
Example
Private Sub BackgroundWorker1_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
If txtLog.InvokeRequired Then
txtLog.Invoke(Sub() txtLog.AppendText("My text" & vbNewLine))
txtLog.Invoke(Sub() txtLog.ScrollToCaret())
Else
txtLog.AppendText("My text" & vbNewLine)
txtLog.ScrollToCaret()
End If
Something in Backgroudworker . . .
If txtLog.InvokeRequired Then
txtLog.Invoke(Sub() txtLog.AppendText("My text" & vbNewLine))
txtLog.Invoke(Sub() txtLog.ScrollToCaret())
Else
txtLog.AppendText("My text" & vbNewLine)
txtLog.ScrollToCaret()
End If
. . .
End Sub
Is there any shorter method that would allow me to write in textbox?
The whole point of using a BackgroundWorker is so that you don't have to call Invoke and use delegates. If you're going to do that anyway then the BackgroundWorker is pointless.
With regards to the code you have, what's the point of testing InvokeRequired when you know for a fact that the DoWork event handler will be executed on a background thread? It's always going to be True. Also, why would you call Invoke twice and add all that overhead when you can call it once and do as many things as you like in the one call?
Private Sub BackgroundWorker1_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
txtLog.Invoke(Sub()
txtLog.AppendText("My text" & vbNewLine))
txtLog.ScrollToCaret()
End Sub)
As I said though, you shouldn't be calling Invoke at all. If you want to do something on the UI thread from a BackgroundWorker then you call ReportProgress and handle the ProgressChanged event. The code you want executed goes in the event handler and you call ReportProgress as required. If you need to pass data, you do so using the second argument to ReportProgress and get it back from e.UserState in the event handler. Here's one I prepared earlier:
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
'Raise the DoWork event in a worker thread.
Me.BackgroundWorker1.RunWorkerAsync()
End Sub
'This method is executed in a worker thread.
Private Sub BackgroundWorker1_DoWork(ByVal sender As Object, _
ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
Dim worker As System.ComponentModel.BackgroundWorker = DirectCast(sender, System.ComponentModel.BackgroundWorker)
For i As Integer = 1 To 100
'Raise the ProgressChanged event in the UI thread.
worker.ReportProgress(i, i & " iterations complete")
'Perform some time-consuming operation here.
Threading.Thread.Sleep(250)
Next i
End Sub
'This method is executed in the UI thread.
Private Sub BackgroundWorker1_ProgressChanged(ByVal sender As Object, _
ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
Me.ProgressBar1.Value = e.ProgressPercentage
Me.Label1.Text = TryCast(e.UserState, String)
End Sub
'This method is executed in the UI thread.
Private Sub BackgroundWorker1_RunWorkerCompleted(ByVal sender As Object, _
ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted
Me.Label1.Text = "Operation complete"
End Sub
The goal is that the form should still be clickable while the code runs (mainly to be able to break the code) AND keeps getting updated by the running code at the same time. First part works, not the second part.
Public Class FormMain
Public PleaseStopAll As Boolean
Private Sub BreakCode_Click(sender As Object, e As EventArgs) Handles BreakCode.Click
PleaseStopAll = True
End Sub
Private Sub Testeur_Click(sender As Object, e As EventArgs) Handles Testeur.Click
PleaseStopAll = False
ThreadPool.QueueUserWorkItem(AddressOf LaunchTask, 0)
End Sub
Sub LaunchTask(o As Object)
TheTask(Me)
End Sub
Public Sub ShowSomeMessage(msg As String
Me.Displayer.text = msg
End Sub
End Class
Module TaskDoer
Sub TheTask(MyMenu As FormMain)
For i As Integer = 0 To 42
whatever() 'some tasks that cannot be interrupted safely
MyMenu.ShowSomeMessage("task #" & CStr(i) & " over") ' Here is the error
If MyMenu.PleaseStopAll Then Exit For
Next
End Sub
End Module
The "status update" line tells that this task cannot interact with its parent task.
If I remove this line, the code runs just fine : code breaks when I ask to, and form remains clickable.
I tried with BackgroundWorker, but it needs actual progression to report something with ReportProgress (in increasing %) do I cannot just give a status (as string)
Pass anything else via a variable or structure
BackgroundWorker1.WorkerReportsProgress = True '<Make sure this is set
BackgroundWorker1.RunWorkerAsync()
Private SomeStatus As String
Private Sub BackgroundWorker1_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
SomeStatus = "Hello"
BackgroundWorker1.ReportProgress(0)
End Sub
Private Sub BackgroundWorker1_ProgressChanged(sender As Object, e As System.ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
ShowSOmeMEssage(SomeStatus)
End Sub
FYI: The progress thing is only so you can call a routine on the main UI thread. You can set as many thread-safe variables as you want on the form from within the backgroundworker. It's just updating the UI that's bothersome, though there is a workaround for that too.
First off, pardon me if my English is bad, I'm not a native English speaker.
I'm fairly new to programming and I'm trying to teach myself VB.NET
I came across a problem while trying to learn about Delegates. (see code below)
What I'm trying to accomplish is to update a specified Control's text property via a thread. However, as soon as I start the thread, I get an ArgumentException Error. I have completely no idea what's wrong. Anybody have an idea what i've done wrong here?
Public Class Form1
Delegate Sub myDelegate1(ByVal s_Name As Control, ByVal s_txt As String)
Public txtUpdate As New myDelegate1(AddressOf upd_ControlTextProperty)
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Label1.Text = vbnullstring
End Sub
Private Sub upd_ControlTextProperty(ByVal ControlName As Control, ByVal txt As String)
ControlName.Text = txt
End Sub
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim thread1 As New Threading.Thread(AddressOf threadstart)
thread1.IsBackground = True
thread1.Start()
End Sub
Private Sub threadstart()
Me.Invoke(Me.txtUpdate, New Object(), {Label1, "This is Label 1"})
End Sub
End Class
As TheValyreanGroup said, your delegate is supposed to accept two arguments, and you pass it three :
Me.Invoke(Me.txtUpdate, New Object(), {Label1, "This is Label 1"})
^-1--------^ ^-2--------^ ^-3-----------------------^
So just remove the New Object() thing, and transform this {Label1, ...} into just a string :
Me.Invoke(Me.txtUpdate, "This is Label 1")
OK Better that way.
On a second hand, what you are doing is not very usefull.
You create a new Thread from your UI Thread.
With this new Thread, you invoke back the UI Thread and you stop your Thread...
Remember that a Control can be updated only by the Thread who created the Form (the UI thread).
Unless you have a good reason to work with your background thread, you can resume your code to :
Public Class Form1
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Label1.Text = vbnullstring
End Sub
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Label1.Text = "This is Label 1"
End Sub
End Class
UPDATE
(from comments)
To make it more clear, here is a schema (that I took on https://androidkennel.org/android-networking-tutorial-with-asynctask/, if any restrictions apply I will remove the image)
The Main UI Thread is used for things :
React to user events (clicks, inputs...) and start background threads that will do the process
Update the User Interface when the background thread is over or during the task.
When I say what you're doing is not usefull is because your background thread does not do any processing, it just signals the UI thread to update the UI...
I would try this approach. upd_ControlTextProperty can be called successfully either from the UI thread or your new thread.
Public Class Form1
Delegate Sub myDelegate1(ByVal s_Name As Control, ByVal s_txt As String)
Public txtUpdate As New myDelegate1(AddressOf upd_ControlTextProperty)
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Label1.Text = ""
End Sub
Private Sub upd_ControlTextProperty(ByVal ControlName As Control, ByVal txt As String)
If Me.InvokeRequired = True Then
Me.Invoke(txtUpdate, New Object() {ControlName, txt})
Else
ControlName.Text = txt
End If
End Sub
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim thread1 As New Threading.Thread(AddressOf threadstart)
thread1.IsBackground = True
thread1.Start()
End Sub
Private Sub threadstart()
upd_ControlTextProperty(Label1, "This is Label 1")
End Sub
End Class
Here is my question in short: How do I use the BackGroundWorker (or InvokeRequired method) to make thread-safe calls to append text to a text box?
Here is my question in with much detail and background: I've been working on a program that copies file from one location to another for backup purposes. I set an option that will save a file when the file is modified using the FileSysteWatcher. Here is the code:
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Me.Load
Dim directoryPath As String = Path.GetDirectoryName(TextBox1.Text)
Dim varFileSystemWatcher As New FileSystemWatcher()
varFileSystemWatcher.Path = directoryPath
varFileSystemWatcher.NotifyFilter = (NotifyFilters.LastWrite)
varFileSystemWatcher.Filter = Path.GetFileName(TextBox1.Text)
AddHandler varFileSystemWatcher.Changed, AddressOf OnChanged
varFileSystemWatcher.EnableRaisingEvents = True
End Sub
Private Sub OnChanged(source As Object, ByVal e As FileSystemEventArgs)
My.Computer.FileSystem.CopyFile(e.FullPath, TextBox2.Text & "\" & e.Name, True)
TextBox3.ApendText("[New Text]") ' This line causes an error
End Sub
The code works fine except for updating the text in textbox3. I need the textbox to update when the file specified is modified. This is important so that the user can know the program is working and to have a complete log of the programs operations.
The error that is caused is:
Cross-thread operation not valid: Control 'TextBox3' accessed from a
thread other than the thread it was created on.
I'm assuming "AddHandler varFileSystemWatcher.Changed, AddressOf OnChanged" creates a new thread. Also, updating "textbox3" within the "OnChange Sub" (in the way I did) is not a thread-safe manner. So I did some research and found this article:
How to: Make Thread-Safe Calls to Windows Forms Controls
The article explains that one can use InvokeRequired or a BackgroundWorker to make thread-safe calls. I would like to use the BackgroundWorker to make a thread-safe call (if InvokeRequired is more efficient then I'll use that), but this portion, of the example provied, leaves me confused:
' Do I need these imports to use the BackgroundWorker or InvokeRequired?
Imports System
Imports System.ComponentModel
Imports System.Threading
Imports System.Windows.Forms
Public Class Form1
Inherits Form ' Do I need this for what I am trying to do?
' This delegate enables asynchronous calls for setting
' the text property on a TextBox control.
Delegate Sub SetTextCallback([text] As String)
' This thread is used to demonstrate both thread-safe and
' unsafe ways to call a Windows Forms control.
Private demoThread As Thread = Nothing
' This BackgroundWorker is used to demonstrate the
' preferred way of performing asynchronous operations.
Private WithEvents backgroundWorker1 As BackgroundWorker
Private textBox1 As TextBox
Private WithEvents setTextUnsafeBtn As Button
Private WithEvents setTextSafeBtn As Button
Private WithEvents setTextBackgroundWorkerBtn As Button
' What is this part of the code for and do I need it?
Private components As System.ComponentModel.IContainer = Nothing
' Again, What is this part of the code for and do I need it?
Public Sub New()
InitializeComponent()
End Sub
' And again, What is this part of the code for and do I need it?
Protected Overrides Sub Dispose(disposing As Boolean)
If disposing AndAlso (components IsNot Nothing) Then
components.Dispose()
End If
MyBase.Dispose(disposing)
End Sub
Many parts of the above code leave me confused. What does it do? What parts of this code do I need to use the BackgroundWorker? What parts for the InvokeRequired method? Again, How do I use the BackGroundWorker (or InvokeRequired method) to make thread-safe calls to append text to a text box? (It'd be great to have the code above explained, but all I really need is one example of how to update the text of a text box in a thread-safe manner.)
Change:
Private Sub OnChanged(source As Object, ByVal e As FileSystemEventArgs)
My.Computer.FileSystem.CopyFile(e.FullPath, TextBox2.Text & "\" & e.Name, True)
TextBox3.ApendText("[New Text]") ' This line causes an error
End Sub
To:
Private Sub OnChanged(source As Object, ByVal e As FileSystemEventArgs)
My.Computer.FileSystem.CopyFile(e.FullPath, TextBox2.Text & "\" & e.Name, True)
AppendTextBox(TextBox3, "[New Text]")
End Sub
Private Delegate Sub AppendTextBoxDelegate(ByVal TB As TextBox, ByVal txt As String)
Private Sub AppendTextBox(ByVal TB As TextBox, ByVal txt As String)
If TB.InvokeRequired Then
TB.Invoke(New AppendTextBoxDelegate(AddressOf AppendTextBox), New Object() {TB, txt})
Else
TB.AppendText(txt)
End If
End Sub
Here's an updated, simplified version that uses an anonymous delegate:
Private Sub OnChanged(source As Object, ByVal e As FileSystemEventArgs)
Dim filename As String = System.IO.Path.Combine(TextBox2.Text, e.Name)
Try
My.Computer.FileSystem.CopyFile(e.FullPath, filename, True)
TextBox3.Invoke(Sub()
TextBox3.AppendText("[New Text]")
End Sub)
Catch ex As Exception
Dim msg As String = "Source: " & e.FullPath & Environment.NewLine &
"Destination: " & filename & Environment.NewLine & Environment.NewLine &
"Exception: " & ex.ToString()
MessageBox.Show(msg, "Error Copying File")
End Try
End Sub
You can also use SynchronizationContext. Be careful to get a reference to it from the constructor. There's a great article on this at CodeProject.com: http://www.codeproject.com/Articles/14265/The-NET-Framework-s-New-SynchronizationContext-Cla
Imports System.IO
Imports System.Threading
Public Class Form1
Private m_SyncContext As System.Threading.SynchronizationContext
Private m_DestinationPath As String
Public Sub New()
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
Me.m_SyncContext = System.Threading.SynchronizationContext.Current
End Sub
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Me.m_DestinationPath = Me.TextBox2.Text
Dim directoryPath As String = Path.GetDirectoryName(TextBox1.Text)
Dim varFileSystemWatcher As New FileSystemWatcher()
varFileSystemWatcher.Path = directoryPath
varFileSystemWatcher.NotifyFilter = (NotifyFilters.LastWrite)
varFileSystemWatcher.Filter = Path.GetFileName(TextBox1.Text)
AddHandler varFileSystemWatcher.Changed, AddressOf OnChanged
varFileSystemWatcher.EnableRaisingEvents = True
End Sub
Private Sub OnChanged(source As Object, ByVal e As FileSystemEventArgs)
My.Computer.FileSystem.CopyFile(e.FullPath, Me.m_DestinationPath & "\" & e.Name, True)
Me.m_SyncContext.Post(AddressOf UpdateTextBox, "[New Text]")
End Sub
Private Sub UpdateTextBox(param As Object)
If (TypeOf (param) Is String) Then
Me.TextBox3.AppendText(CStr(param))
End If
End Sub
End Class
My BackgroundWorker works perfectly in my main form frmMain. But when I run the ReportProgress method in another module, I get exception "This BackgroundWorker states that it doesn't report progress. Modify WorkerReportsProgress to state that it does report progress." This IS set to report progress; this works fine when run the same way in the main module.
Basically, from a module called by my BackgroundWorker, I want to show progress on my main form.
How can I fix this? The only idea I have is to move the code from the module into my main form, but this seems a backward step, which would involve extra work. Am hoping there are easier ways!
Calling code in class frmMain:
Friend WithEvents BackgroundWorker As New System.ComponentModel.BackgroundWorker
Private Sub btnTest_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnTest.Click
' Specify that we do NOT want the background operation to allow cancellation
BackgroundWorker.WorkerSupportsCancellation = False
' Specify that we want the background operation to report progress.
BackgroundWorker.WorkerReportsProgress = True
' Start running the background operation by calling the RunWorkerAsync method.
BackgroundWorker.RunWorkerAsync()
End Sub
Private Sub BackgroundWorker_DoWork(ByVal sender As Object, _
ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker.DoWork
Dim result As Boolean
result = MyTest()
End Sub
Private Sub BackgroundWorker_ProgressChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker.ProgressChanged
Me.Text = e.ProgressPercentage.ToString() & "%"
sspStatus.Text = e.UserState.ToString
End Sub
Private Sub BackgroundWorker_RunWorkerCompleted(ByVal sender As Object, _
ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) _
Handles BackgroundWorker.RunWorkerCompleted
If e.Cancelled = True Then
' The background operation was cancelled
Me.Text = "Cancelled!"
ElseIf e.Error IsNot Nothing Then
' The background operation encountered an error
Me.Text = "Error: " & e.Error.Message
Else
' The background operation completed successfully
Me.text = "Done!"
End If
End Sub
Code which generates the exception in separate module Invoices:
Public Function MyTest() As Boolean
frmMain.BackgroundWorker.ReportProgress(0)
End Function
Am using VB.NET in VS 2010, with .NET 3.5.
Try to set it up as
Public Function MyTest(worker as BackgroundWorker) As Boolean
worker.ReportProgress(0)
End Function
to make sure you are talking to the right worker instance.
(And aside: avoid using classnames for instance fields).