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.
- How to enable special keys (ctrl-c, ctrl-v, tab, delete) Windows.Form.WebBrowser Control
- How do I make the Delete key work in the WebBrowser control
- WebBrowser "steals" KeyDown events from my form
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!