Resolved Closing a Web Browser Control with KeyDown event

ALX

Well-known member
Joined
Nov 16, 2005
Messages
253
Location
Columbia, SC
Programming Experience
10+
This may seem a little trivial, but I'm a nut for making an app work with the keyboard and avoid making the user reach for the mouse when ever possible.

I have a Help page for my app that is shown on a web browser control. This is easy for me as I can update the help page quickly and easily just by modifying the HTML of the Help page. It's also easier to embed images as compared to say maybe a rich textbox. I want the user to be able to close the Help segment just by pressing the Escape key as opposed to clicking the close button. My code for starters...
VB.NET:
Private Sub HelpButton1_Click()                                       '    Called from the Help Icon Click or F1

        If WBPanel Is Nothing Then
            RemoveHandler Me.KeyDown, AddressOf Me_KeyDown            '    This gets rid the the KeyDown Handler for the hosting page
            WBPanel = New Panel 
                      
                                                                      '    code removed here for Panel Size, Location, CloseButton details etc.

            WB1 = New WebBrowser
            Dim n As String = My.Computer.FileSystem.ReadAllText('HTML loaded from disk...)

            With WB1
                .Size = ...
                .Location = ...
                .DocumentText = n
                .Tag = "Dynamic"                                        '    All controls on this panel are assigned a "Dynamic" Tag
                .BringToFront()
            End With

            WBPanel.Controls.Add(WB1)                                   '    Add the Web Control to the panel
            Me.Controls.Add(WBPanel)                                    '    and the panel to the hosting page
            WBPanel.BringToFront()
            CloseButton.BringToFront()
            AddHandler CloseButton.Click, AddressOf CloseWB            '    CloseWB just calls CloseWebBrowser()
            WB1.Document.Focus()
            WBPanel.Show()
        End If
    End Sub

'-------------------------------------------------------------------
private Sub CloseWB()                                         '    Called from the CloseButton Click or the Escape KeyPress

    If WBPanel IsNot Nothing Then
        RemoveHandler CloseLabel.Click, AddressOf CloseWB
        Dim ctlList As New List(Of Control)

        For Each ctl As Control In WBPanel.Controls
            If ctl.Tag = "Dynamic" Then                               '    Create a list of all disposeable controls added to this panel
                ctlList.Add(ctl)
            End If
        Next

        For Each ctl In ctlList
            Me.Controls.Remove(ctl)
            ctl.Dispose()                                             '    Here I get a 'RaceOnRCWCleanup was detected' Message when
            ctl = Nothing                                             '   KeyDown is used and the ctl = the Web Browser
        Next

    WBPanel.Dispose()
        WBPanel = Nothing
        AddHandler Me.KeyDown, AddressOf Me_KeyDown                   '    Restore the previously removed handler...
    End If
End Sub

'-------------------------------------------------------------------
Private Sub WB1_DocumentCompleted(ByVal sender As System.Object, ByVal e As Windows.Forms.WebBrowserDocumentCompletedEventArgs) Handles WB1.DocumentCompleted

    AddHandler WB1.Document.Body.KeyDown, New HtmlElementEventHandler(AddressOf WB1_KeyDown)
End Sub

'-------------------------------------------------------------------
Private Sub WB1_KeyDown(ByVal sender As Object, ByVal e As HtmlElementEventArgs)

    If e.KeyPressedCode = 27 Then CloseWB()
End Sub

If I set this up without the KeyDown event, and force the user to use the 'Close' button, everything works great. With the Keydown event tho, I get an message when closing the WebBrowser: "RaceOnRCWCleanup was detected Message: An attempt has been made to free an RCW that is in use. My guess is that I'm trying to dispose an object for which the KeyDown Event is currently being processed... So I tried the following code:

VB.NET:
Private Sub WB1_KeyDown(ByVal sender As Object, ByVal e As HtmlElementEventArgs)

    If e.KeyPressedCode = 27 Then
    RemoveHandler WB1.Document.Body.KeyDown, AddressOf WB1_KeyDown
    Task.Run(Sub() CloseWebBrowser())
    End If
End Sub

Private Sub CloseWebBrowser()

    Dim t As New DateTime
    t = Now.AddSeconds(2)

    Do
        If DateTime.Now > t Then
            CloseWB()
            Exit Do
        End If
    Loop
End Sub

My thinking was that that this gave time for the event handler to complete and exit the sub... But NO! Now I get a Cross-Thread error in CloseWebBrowser(). This is no longer just my anal attempt to avoid forcing the user to not have to reach for the mouse. I need to beat this thing! I would greatly appreciate any guidance. Thank You !
 
Last edited:
Well I beat the CrossThread error by using a timer rather than the Task.Run scenario.
VB.NET:
Private Sub WB1_KeyDown(ByVal sender As Object, ByVal e As HtmlElementEventArgs)

        If e.KeyPressedCode = 27 Then
            RemoveHandler WB1.Document.Body.KeyDown, AddressOf WB1_KeyDown
            Timer1 = New Timer
            Timer1.Interval = 1
            Timer1.Start()
            AddHandler Timer1.Tick, AddressOf CloseWebBrowser
        End If
    End Sub

    Private Sub CloseWebBrowser(ByVal sender As Object, ByVal e As EventArgs)

        Timer1.Stop()
        Timer1.Dispose()
        RemoveHandler Timer1.Tick, AddressOf CloseWebBrowser
        CloseWB()

    End Sub

'Not very elegant... but it works...
I'm thinking there must be a better way to accomplish this. The timer trick seems tacky. I'm curious to know how this SHOULD be handled.
 
Last edited:
By calling Task.Run, you cause the code to be executed on a background thread. As I would imagine you are aware by now, you cannot directly affect controls on a thread other than the UI thread. If you wanted to use the original code then you'd need to call Invoke or BeginInvoke in order to marshal a call to the UI thread and then close the WebBrowser there. On the subject of being not very elegant, your original code has a busy wait loop, which is the worst code you can possibly write. You could change that original code like so to make it work:
VB.NET:
Private Sub WB1_KeyDown(sender As Object, e As HtmlElementEventArgs)

    If e.KeyPressedCode = 27 Then
        RemoveHandler WB1.Document.Body.KeyDown, AddressOf WB1_KeyDown
        CloseWebBrowser()
    End If
End Sub

Private Async Function CloseWebBrowser() As Task
    Await Task.Delay(2000)

    BeginInvoke(AddressOf CloseWB)
End Function
The call to CloseWebBrowser returns a Task that will wait 2 seconds and then marshal a call to the UI thread to actually close the WebBrowser.
 
This is good ! Thank You JM.
I was aware of the busy loop. I was in a hurry to make something work and had intended to clean it up once I got rid of the errors. 'Was unaware of the 'Task.Delay()' though. It's tough learning this stuff as piece meal through Google.
 
Task.Delay is basically the non-blocking version of Thread.Sleep. If the CloseWebBrowser method is executed on a background thread then you could call Thread.Sleep without issue, but it may only be the delay that causes the that to happen, so safest to stick with Task.Delay.
 
I have always been terrified of Delegates. I just had the hardest time grasping the concept and how to implement them, except of course those that are provided by a simple 'AddressOf' statement for an event handler. I'm sure in the past I have written many extra lines of code just avoiding them. So after all these years, I have finally used a Delegate thanks to JM's push. For other part time coders out there like me, here is how it played out for this part of the project.
At the Global level of the class: Delegate Sub CloseBrowser() This is just the Delegate's name and has no significance as to what it's actual function or purpose is.
*The CloseWBrowser Sub is much the same as it is above but without any parameters.
Then to close the Browser using this Delegate...
VB.NET:
 Private Sub WB1_KeyDown(ByVal sender As Object, ByVal e As HtmlElementEventArgs)
        If e.KeyPressedCode = 27 Then
            RemoveHandler WB1.Document.Body.KeyDown, AddressOf WB1_KeyDown
            Dim unused = CloseWebBrowser()                         '    Visual Studio will force you to use a throw-away variable...  We are, after all, calling a function.
        End If
    End Sub

    Private Async Function CloseWebBrowser() As Task
        Await Task.Delay(100)
        Dim CWB As CloseBrowser                                          '    Visual Studio will force you to assign your Delegate to a variable
        CWB = AddressOf CloseWBrowser                              '    Here you can point this Delegate to any sub that has the same signature (No parameters in this case)
        BeginInvoke(CWB)
    End Function
Reading through Microsoft's literature on this topic is a lot like reading a law book. My eyes just glaze over. Why can they not explain this with a language structure that is understandable?
I have looked into the eyes of the beast... Works like a charm !
Many Thanks JM...
 
Last edited:
Delegates are pretty simple really. Where you can think of a class instance as an object that contains data, where you can pass that object around wherever you want and access that data when and where it's required, a delegate instance is an object that contains a reference to a method, so you can pass that object around wherever you want and then invoke that method when and where it's required.
 
On your CloseWB, disposing the parent Panel will also dispose all its child controls automatically.
Releases the unmanaged resources used by the Control and its child controls
 
Back
Top