0

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?

Michael
  • 1,036
  • 1
  • 11
  • 22
  • 1
    If I am interpreting the code correctly, the only time it cares if it is on the UI thread is the last statement `DocumentTabStrip.InvokeIfRequired` where it potentially adds a control created on a 2ndry thread to `DocumentTabStrip.Controls`. Is that a correct assessment? If so, that is also the problem. – TnTinMn Mar 14 '17 at 17:30
  • @TnTinMn : That is most likely the problem, yes. I just finished an answer about it. – Visual Vincent Mar 14 '17 at 17:39

1 Answers1

1

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)
Visual Vincent
  • 18,045
  • 5
  • 28
  • 75
  • It never occurred to me to wrap the entire method in InvokeIfRequired, only the particular call the caused the problem (the error was only when trying to actually add the control to the DocumentTabStrip). I'm still testing, but this *seems* like it may have fixed the issue. I don't have any experience with multithreaded programming in Winforms (other than what I've learned the past week or so working with this issue). Any chance you can explain in more detail why this works and the previous way didn't? – Michael Mar 14 '17 at 17:54
  • @Locke : But I did explain it :). You are creating the control on a background thread, but you're adding it to the UI thread. Like the error says controls must always be accessed from the thread they were created on. – Visual Vincent Mar 14 '17 at 18:06
  • Ah, I see what you're saying. Even though I was adding it to the proper thread before, I was creating it on the wrong thread. Thanks a million! – Michael Mar 14 '17 at 18:07
  • @Locke : Exactly. Controls should only be created _**and/or**_ accessed from the thread that the target container (form, panel, user control, etc.) runs on. – Visual Vincent Mar 14 '17 at 18:10
  • @Locke : You can actually create forms in different threads, which would sort of cause there to be multiple UI threads and you would have to invoke to the correct one. However creating multiple UI threads _**is not**_ recommended. – Visual Vincent Mar 14 '17 at 18:13
  • Thanks a lot. You saved my bacon. :) – Michael Mar 14 '17 at 19:28