Passing data to different UI threads - vb.net

As an old mainframe systems programmer, I'm not specialised at all in OO thinking, nor in Visual Basic. A former colleague and I wrote a VB program (named FotoDB) to manage and present our family&holyday photos. We have kind of a database with descriptions and GPS coordinates for all of them.
FotoDB can also work in "presentation mode" and then I'd like to also show the OpenStreetMap of the area where the active photo was taken. I already found out how to use Leaflet.js to display the map in a WebBrowser object.
A first problem I encountered when I tried to use a WebBrowser object was that I got:
cannot be instantiated because the current thread is not in a
single-threaded apartment.
Our FotoDB program must remain defined multi-threading (I think): it uses a BackgroundWorker for some things, and is also listening for 2 control files that might be changed by Notepad (or alike).
So, I abandoned the idea of adding the map as a small item on the main screen of FotoDB, but use another form for the WebBrowser object, and I start that in a new STA-mode thread:
mapThread = New Thread(AddressOf form4MapShow)
mapThread.SetApartmentState(ApartmentState.STA)
mapThread.Start()
End Sub
Private Sub form4MapShow()
Form4map.Show()
End Sub
That works fine, I can display a section of the OpenStreetMap in my "Form4map".
I still have 2 problems (probably reduceable to 1 problem)
I need to pass the GPS coordinates from the main program to the thread in which From4map runs
When the end-user selects a new photo to view, the main program must pass the new coordinates to Form4map
I've been reading a bit about Delegate, but without understanding it completely. I found examples about updating a ListView from another thread and tried to mimic it (when a new photo is displayed in the main frame, send its GPS coordinates to a text field on Form4map), So I coded (as a basic test):
NieuweFoto(Form4map.txtNwFoto, "Nieuw " & Now.ToLocalTime)
End Sub
Public Delegate Sub newPhotoInvoker(ByVal txt As TextBox, ByVal itemtext As String)
Public Sub NieuweFoto(ByVal txt As TextBox, ByVal gpsInfo As String)
If Form4map.txtNwFoto.InvokeRequired Then
Form4map.txtNwFoto.Invoke(New newPhotoInvoker(AddressOf NieuweFoto), txt, gpsInfo)
Else
txt.Text = gpsInfo
End If
End Sub
But, now I get a similar error as at the start:
An error occurred creating the form. See Exception.InnerException for details.
The error is: ActiveX control '8856f961-340a-11d0-a96b-00c04fd705a2' cannot be instantiated because the current thread is not in a single-threaded apartment.
So I'm stuck. Is there an easy solution?

Related

Winform controls loaded from CefSharp causing Cross-Thread errors

I'm integrating CefSharp into a legacy Winforms application. Currently the application uses the default .NET browser control. Unfortunately, this control has a memory leak which is causing serious problems. I'm trying to get the CefSharp browser integrated without major refactoring, since the application is scheduled to be retired later this year and replaced with a new WPF application.
So far, I'm able to make CefSharp work in most scenarios. Standard webpages open without a problem. However, some of these pages have specially formed links which, when interpretted by the application, open up .Net forms instead of other webpages. This is where I run into an issue. When the webpage opened in CefSharp calls one of these links and the application attempts to open the new form, it appears to be doing so on the thread hosting the CefSharp instance, which is different from that hosting the application itself. This leads to numerous cross-thread issues (the legacy application in question isn't particularly well architectured). I'm trying to find a way to solve this issue without rearchitecturing the Winform application.
The following is a brief example of the situation.
Controls
frmMain
This is the primary form on the application. It has a number of duties, but the one pertinent to the current situation is that it hosts a Telerik DocumentTabStrip which contains the application's "tabs" (each browser or form opens within one of these tabs). It also contains the method that is used to load the various form or browser controls that are instantiated and add them to the aforementioned DocumentTabStrip.
ucChromeBrowser
This is the object which wraps the CefSharp browser instances which get created. It also has all the Event Handlers for the CefSharp events, such as IRequestHandler, ILifespanHandler, IContextMenuHandler, etc.
EntryScreens.uc_Foo
This is one of the Winform controls that are called from the webpage hosted in ucChromeBrowser. A typical link looks like WEB2WIN:Entryscreens.uc_Foo?AdditionalParameterDataHere. We intercept these calls in frmMain and instead of attempting to open the link in a browser, we instantiate a new instance of EntryScreen.uc_Foo and load that new form into frmMain.DocumentTabStrip as follows.
Dim _DockWindow As Telerik.WinControls.UI.Docking.DocumentWindow = Nothing
Dim _Control As ucBaseControl = Nothing
Dim _WebBrowser As ucBrowser = Nothing
Dim _isWebBrowerLink As Boolean = False
'Do Other Stuff here, such as instantiating the _Control or _WebBrowser instance, setting _isWebBrowserLink, etc.
_DockWindow = New Telerik.WinControls.UI.Docking.DocumentWindow
If _isWebBrowerLink Then
If Not IsNothing(_WebBrowser) Then
_WebBrowser.Dock = DockStyle.Fill
_DockWindow.Controls.Add(_WebBrowser)
End If
Else
_Control.Dock = DockStyle.Fill
_DockWindow.Controls.Add(_Control)
End If
DocumentTabStrip.InvokeIfRequired(Sub() DocumentTabStrip.Controls.Add(_DockWindow))
(In case it matters, here's the InvokeIfRequired method I'm calling.)
Public Module ISynchronizeInvokeExtensions
<Runtime.CompilerServices.Extension>
Public Sub InvokeIfRequired(obj As ISynchronizeInvoke, action As MethodInvoker)
Dim idleCounter As Integer = 0
While Not CType(obj, Control).Visible
'Attempt to sleep since there's no visible control
If idleCounter < 5 Then
Threading.Thread.Sleep(50)
Else
Exit While
End If
End While
If obj.InvokeRequired Then
Dim args = New Object(-1) {}
obj.Invoke(action, args)
Else
action()
End If
End Sub
End Module
The issue comes up when trying to call DocumentTabStrip.InvokeIfRequired(Sub() DocumentTabStrip.Controls.Add(_DockWindow)). From what I can tell, it appears that this changes the layout of the controls hosted within, causing various controls to be instantiated or to have their events called. This, in turn, causes an InvalidOperationException (e.g.: "Cross-thread operation not valid: Control 'pnlLoading' accessed from a thread other than the thread it was created on."). The specific child control varies from Form to Form (so for Form A it might always be pnlLoading which causes it, but for another Form it might be a different control). But most or all of them exhibit this behavior. I have no doubt it's due to the controls themselves being poorly designed, but I don't really have the time to refactor all of them.
So that's the situation. It appears as though the multi-threaded nature of CefSharp is conflicting with the single-threaded nature of the controls in question and causing them to get called on a different thread than they would be otherwise. Is there a way to prevent this?
Controls should only be created on the UI thread, and not in background threads. The error message tells you pretty clearly what's going on.
Cross-thread operation not valid: Control 'pnlLoading' accessed from a thread other than the thread it was created on.
You are accessing the control from the UI thread but due to that it is actually created in a background thread you are performing a cross-thread access since you're invoking to the wrong thread.
Whatever you do you would almost always have to invoke to the background thread when accessing the control, but you cannot do this for any automatic access done by, for instance, the UI message loop.
Therefore you should put all control creation and accessing in the UI only, thus meaning you must put all that code in your first code block in an invoke.
DocumentTabStrip.InvokeIfRequired( _
Sub()
Dim _DockWindow As Telerik.WinControls.UI.Docking.DocumentWindow = Nothing
Dim _Control As ucBaseControl = Nothing
Dim _WebBrowser As ucBrowser = Nothing
Dim _isWebBrowerLink As Boolean = False
'Do Other Stuff here, such as instantiating the _Control or _WebBrowser instance, setting _isWebBrowserLink, etc.
_DockWindow = New Telerik.WinControls.UI.Docking.DocumentWindow
If _isWebBrowerLink Then
If Not IsNothing(_WebBrowser) Then
_WebBrowser.Dock = DockStyle.Fill
_DockWindow.Controls.Add(_WebBrowser)
End If
Else
_Control.Dock = DockStyle.Fill
_DockWindow.Controls.Add(_Control)
End If
DocumentTabStrip.Controls.Add(_DockWindow)
End Sub)

How to update a rich text box from a background worker using a delegate

Ok I'm pretty new to using threads but so far I've managed to get the following:
Private Delegate Sub dlgUpdateText(text as string)
Private Sub UpdateStatus(text as string)
If rtxStatus.InvokeRequired then
Dim dlg as new dlgUpdateText(AddressOf UpdateStatus)
Me.Invoke(dlg, text)
Else
rtxStatus.text = text
End If
End Sub
and from my Async BackgroundWorker I call
UpdateStatus("Some text")
which seems to work ok however in my original code (which generates errors because I'm updating the control from the wrong thread) I used the following code to append to the (rich)textbox:
rtxStatus.Select(rtxStatus.TextLength, 0)
rtxStatus.SelectionColor = Color.Red
rtxStatus.AppendText("Some error occurred gathering data")
My question is how should I modify my new code to allow me to do this rather than just replace the text? I have read several guides on using delegates but I'm still lost on a few points so I don't really know what's going on with the code I have.
Bonus questions (which probably serve best to show what needs explaining to me!):
What does the Delegate Sub actually do? It doesn't seem to serve any purpose other than hold the property (text) that was already passed to the main Sub (UpdateStatus)?
What is happening when Me.Invoke is called? Me is the current form so when I pass the Delegate Sub and the text where is it specified that the text should be passed to the rtxSTatus.Text property?
UpdateStatus runs in the main thread (?) and is called from one of the background worker threads so why is the Invoke even necessary or is the UpdateStatus Sub run under the background thread that called it? When I altered the text that is applied in the Else statement to see which was run it seems that Invoke is never used to change the text in the box.
Any help would be really appreciated, I'm completely baffled by this - thanks!
Rather than creating a delegate I would suggest using the existing methods offered from a backgroundworker. The backgroundworker provides two methods to access the main thread:
The ProgressChanged event to update the main thread during backgroundworker processing and the RunWorkerCompleted event to update the main thread once the backgroundworker process is complete.
You can find this information and how to implement it from the following link:
http://msdn.microsoft.com/en-us/library/ywkkz4s1.aspx

Update UI form from worker thread

I am new to multi-threading in VB.NET and have come across a problem whereby I am wanting to append text to a text box on a form from a service thread running in the background.
The application I am developing is a client/server listener, I have been able to get the client and server PC's to talk with each other (confirmed through MsgBox), however I am now struggling to get the service thread on the server to append the text to the textbox, nothing vissible occurs.
I have a form named testDebug which calls a class (RemoteSupport), this class does all the handshake tasks and updates the textbox with the connection data.
Can anyone identify where I am going wrong and point me in the right direction?
The following is the code I have:
The form has a textbox named txtOutput, the following is from the remoteSupport class
Dim outMessage As String = (encoder.GetString(message, 0, bytesRead))
MsgBox(outMessage, MsgBoxStyle.Information, "MEssage Received")
If outMessage IsNot Nothing Then
If testDebug.InvokeRequired Then
' have the UI thread call this method for us
testDebug.Invoke(New UpdateUIDelegate(AddressOf HandleClientComm), New Object() {outMessage}) '
Else
testDebug.txtOutput.AppendText(outMessage)
End If
'RaiseEvent MessageReceived(outMessage) // a previous attempt to use custom events
End If
I am not sure if the invoke method is the ideal solution or if custom events are, I have spent some time on trying to get custom events to work, but these didnt work either.
// In the RemoteSupport class
Public Delegate Sub MessageReceivedHandler(ByVal message As String)
Public Shared Event MessageReceived As MessageReceivedHandler
// Located throughout the RemoteSupport class where debug information is required.
RaiseEvent MessageReceived(outMessage)
// Located in the code-behind of the form
Private Sub Message_Received(ByVal message As String)
testDebugOutput(message) // this is a function I have created
// to append the text to the text box
End Sub
The code supplied has been cut down so if there is anything else that you want to see or any questions please let me know.
Thanks for your assistance.
EDIT: I have uploaded the two VB files (form and class) to my site, I would appreciate it if someone could have a look at it to help me with identifying the problem with the UI not updating.
I have tried a few other things but nothing seems to be updating the UI once the worker thread has started.
Form: mulholland.it/testDebug.vb.txt
Class: mulholland.it/remoteSupport.vb.txt
Thanks for your assistance.
Matt
I have a form named testDebug...
If testDebug.InvokeRequired Then
This is a classic trap in VB.NET programming. Set a breakpoint on the If statement. Notice how it returns False, even though you know that the code is running on another thread?
InvokeRequired is an instance property of a Form. But testDebug is a class name, not a reference to an instance of a form of type testDebug. That this is possible in VB.NET has gotten a lot of VB.NET programmers in deep trouble. It is an anachronism carried over from VB6. It completely falls apart and blows up in your face when you do this in a thread. You'll get a new instance of the form, instead of the one that the user is looking at. One that isn't visible because its Show() was never called. And otherwise dead as a doornail since the thread isn't running a message loop.
I answered this question several times already, with the recommended fix. I'll just refer you to them rather than rehashing it here:
Form is not updating, after custom class event is fired
Accessing controls between forms
The Delegate method is likely the way you want to go, but I don't see the declaration of the UpdateUIDelegate anywhere
I believe your code should look something like this (assuming you have a reference to the testdebug form local to your remotesupport class
Dim outMessage As String = (encoder.GetString(message, 0, bytesRead))
MsgBox(outMessage, MsgBoxStyle.Information, "MEssage Received")
If outMessage IsNot Nothing Then
If testDebug.InvokeRequired Then
' have the UI thread call this method for us
testDebug.Invoke(New MessageReceivedHandler(AddressOf Testdebug.Message_Received), New Object() {outMessage})
Else
testDebug.txtOutput.AppendText(outMessage)
End If
end if

Active form in a Windows application?

I am developing a Windows Forms application.
I have four forms which is inherited from Baseform in another project.
In all four forms I am using a label to show some transaction count based on network status. I have implemented a thread which gets the active form of application and setting up the text. The code works fine if application screen is active. If I minimize and open any other application, I am getting an null error exception.
How do I get the active form of an application?
Private Sub StartThread()
pollThread =New Thread(AddressOf PollfileStatus)
pollThread.IsBackground =True
running =True
pollThread.Start()
End Sub
Private Sub PollfileStatus()
While (running)
Try
For Each e As Control In Me.ActiveForm.Controls
If (e.Name = "pbStatus") Then
e.Invoke(New SetTextCallback(AddressOf Settext),
New Object() {e, 10})
End If
Next
Catch ex As Exception
Throw New ApplicationException(ex.Message)
End Try
Thread.Sleep(6000)
End While
End Sub
Understandably Me.ActiveForm is empty, since minimizing your form makes it inactive. You have two options:
Test if Me.ActiveForm is empty, and if so, do not update the label. This will effectively 'pause' label updates, until the user restores the window again.
Create a property on your class that contains your thread, to pass a reference of the form you have to update. This way the label on the given form will update even if it is not the active form. This might be a better option, it will update even if the form is inactive in the background, for example.
You should look into the Application.OpenForms shared property. It contains a collection of all forms in your application.
EDIT: Since you're working with .net 1.1 and don't have access to Application.OpenForms, here are some suggestions:
Implement your own shared class (module) that contains an ArrayList of forms. You could then create a base class inheriting from Form, handle the Load event to add the current form to the list of forms and the Closed event to remove it. Once you have that class, have all your real forms derive from it. BTW, it's (almost) how it is done in .Net 2.0.
Turn the problem around and have the working thread raise events when the values change that you can handle in each form to update it.
An old question, but I'd thought I'd offer a suggestion as well: you could implement a Public Shared CurrentForm As Form that you set in the Form.Activated event of a base form class all your forms inherit from. From the thread you could then use CurrentForm to access the desired form.
This is very easy like this in vb.net
Dim MYFormName As String = Me.Name.ToString
You have many solutions for this situation:
first of all you should consider checking if the current form is null before proceeding in any action.
second: if that condition passes, a good approach to store the last active form's reference.
This might work but I cannot test it right now.
For Each frm As Form In Application.OpenForms
If frm.ContainsFocus Then
Return frm
End If
Next

Force multi-threaded VB.NET class to display results on a single form

I have a windows form application that uses a Shared class to house all of the common objects for the application. The settings class has a collection of objects that do things periodically, and then there's something of interest, they need to alert the main form and have it update.
I'm currently doing this through Events on the objects, and when each object is created, I add an EventHandler to maps the event back to the form. However, I'm running into some trouble that suggests that these requests aren't always ending up on the main copy of my form. For example, my form has a notification tray icon, but when the form captures and event and attempts to display a bubble, no bubble appears. However, if I modify that code to make the icon visible (though it already is), and then display the bubble, a second icon appears and displays the bubble properly.
Has anybody run into this before? Is there a way that I can force all of my events to be captured by the single instance of the form, or is there a completely different way to handle this? I can post code samples if necessary, but I'm thinking it's a common threading problem.
MORE INFORMATION: I'm currently using Me.InvokeRequired in the event handler on my form, and it always returns FALSE in this case. Also, the second tray icon created when I make it visible from this form doesn't have a context menu on it, whereas the "real" icon does - does that clue anybody in?
I'm going to pull my hair out! This can't be that hard!
SOLUTION: Thanks to nobugz for the clue, and it lead me to the code I'm now using (which works beautifully, though I can't help thinking there's a better way to do this). I added a private boolean variable to the form called "IsPrimary", and added the following code to the form constructor:
Public Sub New()
If My.Application.OpenForms(0).Equals(Me) Then
Me.IsFirstForm = True
End If
End Sub
Once this variable is set and the constructor finishes, it heads right to the event handler, and I deal with it this way (CAVEAT: Since the form I'm looking for is the primary form for the application, My.Application.OpenForms(0) gets what I need. If I was looking for the first instance of a non-startup form, I'd have to iterate through until I found it):
Public Sub EventHandler()
If Not IsFirstForm Then
Dim f As Form1 = My.Application.OpenForms(0)
f.EventHandler()
Me.Close()
ElseIf InvokeRequired Then
Me.Invoke(New HandlerDelegate(AddressOf EventHandler))
Else
' Do your event handling code '
End If
End Sub
First, it checks to see if it's running on the correct form - if it's not, then call the right form. Then it checks to see if the thread is correct, and calls the UI thread if it's not. Then it runs the event code. I don't like that it's potentially three calls, but I can't think of another way to do it. It seems to work well, though it's a little cumbersome. If anybody has a better way to do it, I'd love to hear it!
Again, thanks for all the help - this was going to drive me nuts!
I think it is a threading problem too. Are you using Control.Invoke() in your event handler? .NET usually catches violations when you debug the app but there are cases it can't. NotifyIcon is one of them, there is no window handle to check thread affinity.
Edit after OP changed question:
A classic VB.NET trap is to reference a Form instance by its type name. Like Form1.NotifyIcon1.Something. That doesn't work as expected when you use threading. It will create a new instance of the Form1 class, not use the existing instance. That instance isn't visible (Show() was never called) and is otherwise dead as a doornail since it is running on thread that doesn't pump a message loop. Seeing a second icon appear is a dead give-away. So is getting InvokeRequired = False when you know you are using it from a thread.
You must use a reference to the existing form instance. If that is hard to come by (you usually pass "Me" as an argument to the class constructor), you can use Application.OpenForms:
Dim main As Form1 = CType(Application.OpenForms(0), Form1)
if (main.InvokeRequired)
' etc...
Use Control.InvokeRequired to determine if you're on the proper thread, then use Control.Invoke if you're not.
You should look at the documentation for the Invoke method on the Form. It will allow you to make the code that updates the form run on the thread that owns the form, (which it must do, Windows forms are not thread safe).
Something like
Private Delegate Sub UpdateStatusDelegate(ByVal newStatus as String)
Public sub UpdateStatus(ByVal newStatus as String)
If Me.InvokeRequired Then
Dim d As New UpdateStatusDelegate(AddressOf UpdateStatus)
Me.Invoke(d,new Object() {newStatus})
Else
'Update the form status
End If
If you provide some sample code I would be happy to provide a more tailored example.
Edit after OP said they are using InvokeRequired.
Before calling InvokeRequired, check that the form handle has been created, there is a HandleCreated property I belive. InvokeRequired always returns false if the control doesn't currently have a handle, this would then mean the code is not thread safe even though you have done the right thing to make it so. Update your question when you find out. Some sample code would be helpful too.
in c# it looks like this:
private EventHandler StatusHandler = new EventHandler(eventHandlerCode)
void eventHandlerCode(object sender, EventArgs e)
{
if (this.InvokeRequired)
{
this.Invoke(StatusHandler, sender, e);
}
else
{
//do work
}
}