0

Special keys such as TAB, DEL, Ctrl+C, Ctrl+V, function keys, etc. do not work in my C# Windows Forms code, which has a WebBrowser control.

These keys work fine in an ordinary Windows forms application with a WebBrowser control. However I am attempting to do something a little different:

  • Provide a C# DLL, which a non-GUI application can use to show the Windows Form.
  • In order not to block the caller, the form is created and shown in a new STA thread.
  • For thread safety, the Document.InvokeScript function is called via a MethodInvoker.

It's not clear which of the above aspects (if any) causes the special keystrokes to be eaten.

I have consulted the following, which don't seem to answer this question.

In particular:

  • Setting WebBrowserShortcutsEnabled = true does not help.
  • Setting the WebBrowser's Url before showing the form does not help.

The HTML and JavaScript are as follows:

<!DOCTYPE html>
<html lang='en'>
<head>
<title>Keyboard Test</title>
<meta charset='utf-8'>
<script>
  document.onkeydown = function (e) {
    var keyCodes = document.getElementById('keyCodes');
    keyCodes.innerHTML += e.keyCode + ', ';
  }
  function JavaScriptFn(arg) {
    alert('JavaScriptFn received arg: ' + arg);
  }
  function CallCSharp() {
    if (window.external && window.external.SupportsAcmeAPI) {
      var str = window.external.CSharpFn('JavaScript says hello');
      alert('C# returned: ' + str);
    }
    else {
      alert('Browser does not support the Acme C# API.')
    }
  }
</script>
</head>
<body>
<select autofocus>
  <option>ABC</option>
  <option>DEF</option>
</select><br>
<input type='text'><br>
<button onclick='CallCSharp();'>Press to Call C#</button>
<p>Press TAB, DEL, Ctrl, etc. to test keyboard support.</p>
<p id='keyCodes'>Key codes detected: </p>
</body>
</html>

The document.onkeydown handler defined in the script above does work when the HTML is loaded in an ordinary browser. All of the special keys work fine in that scenario.

The C# test code is as follows:

// Compile and test as follows (changing paths as needed):
//   set DOTNET=C:\Windows\Microsoft.NET\Framework\v4.0.30319
//   set REF="C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\WindowsBase.dll"
//   %DOTNET%\csc -r:%REF% TestProgram.cs
//   TestProgram.exe

using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Windows.Threading; // WindowsBase.dll
using System.Windows.Forms;

namespace TestProgram
{
  class TestProgram
  {
    static void Main()
    {
      // Write the test HTML file.
      var htmlFile = System.IO.Path.GetTempPath() + "temp.html";
      File.WriteAllText(htmlFile, GetTestHTML());

      // Show the Windows form with the WebBrowser control.
      var wrapper = new FormWrapper(htmlFile);
      wrapper.Run();

      // Call one of the HTML file's JavaScript functions.
      var script = "JavaScriptFn";
      var scriptArgs = new object[] {"C# says hello."};
      if (! wrapper.CallJavaScript(script, scriptArgs))
      {
         MessageBox.Show("An error occurred when calling JavaScript.");
      }

      // What would be a clean way of terminating wrapper's thread?
      Console.WriteLine("Press Ctrl+C to exit ...");
    }

    static string GetTestHTML()
    {
      return "<!DOCTYPE html>\r\n"
        + "<html lang='en'>\r\n"
        + "<head>\r\n"
        + "<title>Keyboard Test</title>\r\n"
        + "<meta charset='utf-8'>\r\n"
        + "<script>\r\n"
        + "  document.onkeydown = function (e) {\r\n"
        + "    var keyCodes = document.getElementById('keyCodes');\r\n"
        + "    keyCodes.innerHTML += e.keyCode + ', ';\r\n"
        + "  }\r\n"
        + "  function JavaScriptFn(arg) {\r\n"
        + "    alert('JavaScriptFn received arg: ' + arg);\r\n"
        + "  }\r\n"
        + "  function CallCSharp() {\r\n"
        + "    if (window.external && window.external.SupportsAcmeAPI) {\r\n"
        + "      var str = window.external.CSharpFn('JavaScript says hello');\r\n"
        + "      alert('C# returned: ' + str);\r\n"
        + "    }\r\n"
        + "    else {\r\n"
        + "      alert('Browser does not support the Acme C# API.')\r\n"
        + "    }\r\n"
        + "  }\r\n"
        + "</script>\r\n"
        + "</head>\r\n"
        + "<body>\r\n"
        + "<select autofocus>\r\n"
        + "  <option>ABC</option>\r\n"
        + "  <option>DEF</option>\r\n"
        + "</select><br>\r\n"
        + "<input type='text'><br>\r\n"
        + "<button onclick='CallCSharp();'>Press to Call C#</button>\r\n"
        + "<p>Press TAB, DEL, Ctrl, etc. to test keyboard support.</p>\r\n"
        + "<p id='keyCodes'>Key codes detected: </p>\r\n"
        + "</body>\r\n"
        + "</html>\r\n";
    }
  }

  public class FormWrapper
  {
    private Form1 form;
    private string htmlFile;

    public FormWrapper(string htmlFile)
    {
      this.htmlFile = htmlFile;
    }

    public void Run()
    {
      // Set up a thread in which to show the Windows form.
      Thread thread = new Thread(delegate()
      {
        // Set up the Windows form, and show it.
        form = new Form1(htmlFile);
        form.Width = 400;
        form.Show();

        // Process the event queue in a loop.
        System.Windows.Threading.Dispatcher.Run();
      });

      // The thread must run in a single-threaded apartment.
      thread.SetApartmentState(ApartmentState.STA);

      // Start the thread.
      thread.Start();
    }

    public bool CallJavaScript(string script, object[] args)
    {
      var success = false;
      for (int i = 0; i < 10; ++i)
      {
        if (form == null)
        {
          // Perhaps the form is in the process of being created?
          Thread.Sleep(100);
        }
        else
        {
          success = form.CallJavaScript(script, args);
          break;
        }
      }
      return success;
    }
  }

  public class Form1 : Form
  {
    private WebBrowser browser;

    public Form1(string htmlFile)
    {
      browser = new WebBrowser();
      browser.Dock = DockStyle.Fill;
      browser.Url = new Uri(htmlFile);
      browser.ObjectForScripting = new ScriptingObject();
      browser.WebBrowserShortcutsEnabled = true; // Does not help.
      Controls.Add(browser);
    }

    // CallJavaScript:  Intended to be a thread-safe call to a JavaScript
    // function in the form's WebBrowser control's HTML document.
    // REFERENCES: 
    // https://stackoverflow.com/questions/315938/webbrowser-document-cast-not-valid
    // http://msdn.microsoft.com/en-us/library/system.windows.forms.htmldocument.invokescript(v=vs.100).aspx
    // http://msdn.microsoft.com/en-us/library/system.windows.forms.methodinvoker(v=vs.100).aspx
    public bool CallJavaScript(string script, object[] args)
    {
      if (!this.IsHandleCreated && !this.IsDisposed)
      {
        return false;
      }
      else
      {
        this.Invoke(new MethodInvoker(()=>browser.Document.InvokeScript(script, args)));
        return true;
      }
    }
  }

  [ComVisible(true)]
  public class ScriptingObject
  {
    // SupportsAcmeAPI:  Visible to JavaScript as window.external.SupportsAcmeAPI.
    public bool SupportsAcmeAPI()
    {
      return true;
    }

    // CSharpFn:  Visible to JavaScript as window.external.CSharpFn.
    public string CSharpFn(string arg)
    {
      MessageBox.Show("CSharpFn received arg: " + arg);
      return "Hello, JavaScript.";
    }
  }
}

Any advice would be appreciated!

Community
  • 1
  • 1
Spacewaster
  • 438
  • 3
  • 13

1 Answers1

3

If you change: System.Windows.Threading.Dispatcher.Run(); to Application.Run(form);

and also the document.onkeydown to:

String html = 
@"
//...
      document.onkeydown = function (e) {
        var keyCodes = document.getElementById('keyCodes');
        e = e || window.event;
        var kc = (e.charCode ? e.CharCode : (e.which) ? e.which : e.keyCode);
        keyCodes.innerHTML += kc + ', ';
      }
//...
";

Then it works. (Side note: You can use a continuous string block @"..." instead of "...\r\n" +)

Loathing
  • 5,109
  • 3
  • 24
  • 35
  • Answer and side note both worked like a charm. Additionally, there is no longer any need in Main() for the user to press Ctrl+C. When the user closes the form, the console application also exits. Thank you! – Spacewaster Sep 04 '14 at 00:33