Tuesday, March 18, 2008

Last week I made 2 minesweeper games, which were fun and all, but the overwhelming response from my friends was "wtf! can't flag tiles with right click!". It took all the fun out of the game.

This weekend I noticed that Google Docs makes nice use of right click gestures and my mission began- enable flagging tiles via right-click in Minesweeper!

Easier said than done, but with the help of some previous investigators in Flash and Silverlight, I was able to hack out a passable solution that works on IE and Firefox (OS X & Windows). Alas, Safari has beaten me once again.

Once I was able to get the right-click event, I needed to figure out which element the mouse clicked on. In Silverlight 2 there's now a HitTest API which made this easy- just hit test with the mouse position and get the first element:
UIElement element in Application.Current.RootVisual
.HitTest(new Point(e.OffsetX, e.OffsetY))
The next step was to figure out a good strategy for propagating the event. In WPF the RoutedEvent class fits the bill perfectly- it's allows propagating any event through the visual tree. Unfortunately even though RoutedEvents are in Silverlight, they are not extensible and I couldn't add my own. Bummer.

I ended up implementing my own variant of RoutedEvents, BubblingEvents (for lack of a better name) and made a super minimal version of them for Silverlight. The code is similar enough to RoutedEvents to be able to start quickly.

To declare a new BubblingEvent:
public static readonly BubblingEvent<ContextMenuEventArgs> ContextMenuEvent
= new BubblingEvent<ContextMenuEventArgs>("ContextMenu", RoutingStrategy.Bubble);
This declares a new event with event args of type ContextMenuEvent which will bubble up the visual tree.

Handling the event is very similar to WPF where the handler is set up for the type:
ContextMenuGenerator.ContextMenuEvent.
RegisterClassHandler(typeof(Page),
Page.HandleContextMenuEvent, false);

private static void HandleContextMenuEvent(object sender, ContextMenuEventArgs e) {
((Page)sender).OnContextMenu(e);
}

protected virtual void OnContextMenu(ContextMenuEventArgs e) {
e.Handled = true;
}
Then it's just a matter of flagging the tiles from the event. The event propagation automatically stops when the handled flag is set to true, unless you register for all events- just like WPF's as well.

Once I got this part going then I realized that my mouse wheel handling code could really benefit from this mechanism as well; it was a very easy modification to convert it to use BubblingEvents.

The cool part is that since the event handling registration is done based on types, I could easily and efficiently create a helper that would register for all MouseWheel events on every ScrollViewer in an entire application:
public static class ScrollableScrollViewer {
private static bool initialized = false;

public static void Initialize() {
if (!ScrollableScrollViewer.initialized) {
MouseWheelGenerator.MouseWheelEvent.
RegisterClassHandler(typeof(ScrollViewer),
ScrollableScrollViewer.HandleMouseWheel, false);
ScrollableScrollViewer.initialized = true;
}
}

private static void HandleMouseWheel
(object sender, MouseWheelEventArgs e) {
ScrollViewer sv = (ScrollViewer)sender;

double verticalOffset = sv.VerticalOffset;
if (e.Delta > 0 && verticalOffset > 0) {
sv.ScrollToVerticalOffset(verticalOffset - e.Delta * 50);
e.Handled = true;
}
else if (e.Delta < 0 && verticalOffset < sv.ScrollableHeight) {
sv.ScrollToVerticalOffset(verticalOffset - e.Delta * 50);
e.Handled = true;
}
}
}
One call to ScrollableScrollViewer.Initialize() and scrollbars now work 'as expected' :)

Code files used here:
BubblingEvent.cs- implementation of extensible routed events in Silverlight.
ContextMenuGenerator.cs- provides bubbling ContextMenu (right-click) events in Silverlight.
MouseWheelGenerator.cs- provides bubbling MouseWheel events in Silverlight.
ScrollableScrollViewer.cs- makes all scrollviewers in an application scroll with the mouse wheel.

Samples:
Right-click enabled Minesweeper.
Test app for scrolling scrollbars and right click. Source