Improved Silverlight Debugging in FireFox
2010-11-18
I'm a fan of Silverlight and the Microsoft stack, but when it comes to browsers I think FireFox is king. Paired with FireBug's javascript console and network traffic monitor, it is an extremely powerful combination for development. Since I work almost exclusively in Silverlight I find that I hit Ctrl-F5 hundreds of times a day and my browser window generally looks something like this:

As a keyboard addict, I would normally hit Ctrl-W ten times to quickly close all those extra windows, but herein lies the problem. When a Silverlight embed is focused, none of those Key Presses bubble up to the browser window. So now I have to manually click each 'x' to close those windows. Similarly, when I have rebuilt my code in Visual Studio and want to quickly reload the page by hitting F5 and... get nothing. Or when I'm trying to verify my resources are loading correctly and need to bring up the FireBug console quickly before the xap loads: F12 to no effect. These are some of the most annoying parts of my job (yes, I have it pretty good).
Solving the refresh part was easy enough, just add HtmlPage.Window.Eval("window.location.reload();");. This works in Chrome and FireFox. F5 always worked in IE8 and is not a concern. Solving the other problems requires somehow restoring focus to FireFox. I noticed that HtmlPage.Document.Body.Focus() works as long as there is at least one focusable element on the page (like a textarea or a button). The test page generated by Visual Studio doesn't and I don't create Web projects until I need them, so I kept looking. I came up with the following Javascript function:
function HtmlFocusManager() {
var list = document.getElementsByTagName('input');
if (list == null || list.length == 0) {
var elem = document.createElement('input');
elem.setAttribute('type', 'button');
elem.setAttribute('style', 'position:absolute; margin:-5000px 0 0 0;');
document.body.appendChild(elem);
elem.focus();
} else {
list[0].focus();
}
document.body.focus();
}
This detects if an input is on the page, and if none is found it inserts a new one. It then apply some CSS rules to position it offscreen. Finally, we need to detect the key presses in Silverlight and call the Javascript method. I wrote up this simple static class to do this:
using System;
using System.Windows;
using System.Windows.Browser;
using System.Windows.Input;
namespace Pop.Silverlight
{
public static class HtmlFocusManager
{
static bool _isCtrlDown;
static bool _isHtmlFocusManagerEvaluated;
public static event EventHandler PageFocused;
static void OnPageFocused()
{
_isCtrlDown = false;
if (PageFocused != null)
{
PageFocused(null, null);
}
}
const string HtmlFocusManagerMethod = @"function HtmlFocusManager() {
var list = document.getElementsByTagName('input');
if (list == null || list.length == 0) {
var elem = document.createElement('input');
elem.setAttribute('type', 'button');
elem.setAttribute('style', 'position:absolute; margin:-5000px 0 0 0;');
document.body.appendChild(elem);
elem.focus();
} else {
list[0].focus();
}
document.body.focus();
}
";
public static void Initialize()
{
var ui = Application.Current.RootVisual as UIElement;
if (ui != null)
{
ui.KeyDown += InputKeyDown;
ui.KeyUp += InputKeyUp;
}
}
static void InputKeyUp(object sender, KeyEventArgs e)
{
switch (e.Key)
{
case Key.Ctrl:
_isCtrlDown = false;
break;
}
}
static void InputKeyDown(object sender, KeyEventArgs e)
{
switch (e.Key)
{
case Key.Ctrl:
_isCtrlDown = true;
break;
default:
break;
}
var keyVal = (int)e.Key;
if (e.Key == Key.F5)
{
HtmlPage.Window.Eval("window.location.reload();");
OnPageFocused();
}
else if (
e.Key == Key.Escape ||
keyVal >= 56 && keyVal <= 67 || // function keys
(_isCtrlDown && (e.Key == Key.W || e.Key == Key.Tab || e.Key == Key.T))
)
{
if (!_isHtmlFocusManagerEvaluated)
{
HtmlPage.Window.Eval(HtmlFocusManagerMethod);
_isHtmlFocusManagerEvaluated=true;
}
HtmlPage.Window.Invoke("HtmlFocusManager", null);
OnPageFocused();
}
}
}
}
Finally, drop the following bit of code in Application_Startup:
private void Application_Startup(object sender, StartupEventArgs e)
{
this.RootVisual = new MainPage();
HtmlFocusManager.Initialize();
}
It's not quite a perfect solution though, because you will have to hit the desired key twice for it to take effect. What are the next steps? Take this code, put it in all you Silverlight applications, and spread the word. Also, this solution doesn't work in Chrome, so if you have any ideas, please leave a comment below.