Winform controls loaded from CefSharp causing Cross-Thread errors - vb.net

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)

Related

Passing data to different UI threads

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?

Using a thread with designer objects vb.net

I have a big problem. I am currently designing an Antivirus, and it is coming along very well. But having all the scanning engines running on the same thread, I.E. the main one, is causing the app to lag in loading, and to become unresponsive during processes. I have tried implementing multithreading to increase the speed and overall performance of my application. But, every time that I try, i get the error of cross threading, I.E. I cannot use the form designers progress bars, buttons and labels etc. I just want to know why this error is thrown up, and how to fix it.
Thanks in Advance!
Use InvokeRequired to check which thread you are calling from, if you're not in the UI thread then InvokeRequired is True, and so you can invoke a delegate from the UI Thread to safely alter the Control:
Public Sub SetText(ByVal text As String)
If (Me.InvokeRequired) Then
'Invoke a delegate from the UI Thread
Me.Invoke(DirectCast(Sub() Label1.Text = "Test", MethodInvoker))
Else
Button1.Text = text
End If
End Sub
It is unsafe to call a control from a thread other than the one that created the control without using the Invoke method. Take a look at this example: http://msdn.microsoft.com/library/ms171728%28v=vs.110%29.aspx
Set your form property: CheckForIllegalCrossThreadCalls to false. The you are getting no errors any more.
Furthermore you must show, that you get a reference between your Thread and your form Controls. Otherwise you change the Controls of the Thread handled window (which you don't see).
Try to write Methods or Functions with By Ref parameter, to share your controls.

Operating a runtime webbrowser control from a background thread vb.net

I'm trying to use a bunch of webbrowsers in some background threads. This works no problem when I use webbrowser controls that i have placed on the form in design view but now when they are created at runtime.
I declare the webbrowsers array globally:
Dim webbroswers(-1) As WebBrowser
The following code is on the main thread:
ReDim Preserve webbroswers(somenum)
For i = 0 To sumnum
webbroswers(currentbrowsermax + i) = New WebBrowser
Next
Then this code is run on the background thread:
If webbroswers(num).InvokeRequired Then
webbroswers(num).Invoke(Sub() webbroswers(num).Navigate(someurl))
Else
webbroswers(num).Invoke(Sub() webbroswers(num).Navigate(someurl))
The program crashes at this point with the following error:
Unable to get the window handle for the 'WebBrowser' control. Windowless ActiveX controls are not supported.
Any help on this would be great. Also if anyone knows how to suppress script errors then I think this might help. I've tried: WebBrowser(num).ScriptErrorsSuppressed = True but this doesn't work (it doesn't work elsewhere in my code when running on the main thread either) Thanks!
The Control.InvokeRequired and Invoke members use the Handle property to figure out what thread owns the control. Problem is, the Handle is null for the web browsers that you created. A control only has a valid handle when you made it visible on a form. Which you didn't do. It will then try to create the handle but that's a fail whale, an ActiveX control like WebBrowser needs a valid Parent. Without Me.Control.Add(), as was done in your original version, it won't have one.
The workaround is simple, you just need another control with a valid Handle property. Any will do, it only cares about the thread that owns the handle, not the value of the handle.
You have one: your form. Use Me.InvokeRequired and Me.Invoke() instead. Or Application.OpenForms(0) if you can't easily get a reference to the form object, best avoided.

Application process stays loaded after exit

We have an application that allows custom UI and interaction via SDK.
A DLL is developed for this purpose using VB.Net and the SDK.
An object variable refers to the application and there are some other object variables for components within the application.
Application allows assigning VBScript code to button(s) displayed in toolbar. The VBScript code is:
Dim Utility_Main
Set Utility_Main = CreateObject("Utility.Application")
Utility_Main.Launch()
This launches a form (custom UI) and users can interact with the application via this form.
Although application itself has its own UI, this utility form is created for database lookup, preserving certain attributes of application objects etc.
In just about every exit point of the form, a procedure is called to unset object variables for application and its components using following code:
========================================
Try
Marshal.ReleaseComObject(objX)
Catch
End Try
Try
objX = Nothing
GC.Collect()
Catch
End Try
Note1: ReleaseComObject and setting object variable to Nothing was wrapped in "If (Not objX is Nothing) Then". But it was changed to above format to make sure it gets called.
Note2: GC.Collect was added later to force GC.
========================================
This is done for each object in reverse order of object hierarchy.
Application's executable (Application.exe) remains loaded in both of the following cases:
Application is closed first and then Utility form is closed
Utility form is closed first and then the application is closed
The only time "Application.exe" goes away is if Application is closed first and then Utility form is closed by clicking on "End Task" in Task Manager.
Any help will be greatly appreciated.
I realize you say that it was split up for some reason, but this should be:
Try
If objX IsNot Nothing Then
Marshal.ReleaseComObject(objX)
End If
Catch e As ArgumentException
' hopefully you have some debug output here
Finally
objX = Nothing
GC.Collect() ' really doubt this is necessary
End Try
This guarantees that objX is released if it is not Nothing, and if that throws an exception (note the list of exceptions), then you can catch it and figure out what happened. Whether or not an exception is thrown, objX will be set to Nothing and the GC will be invoked.
It's probably not this code that is causing your program to stay open. You will need to show more code (how they interact, or any non-daemon threads that you may manually start will also be to blame). It sounds like Utility is forcing the Application to stay alive with a non-daemon, background thread, but it really could be the other way around, and it could be the case that doing that in both directions.

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