Adding mouse wheel scrolling in Silverlight

I have just gone through a project that used mouse wheel scrolling of different elements in the application. There are probably a LOT of different ways of doing this, and this is absolutely not something new. But I wanted to make the solution re-usable by using attached properties.

The only issue with handling the scroll wheel is that Silverlight doesn’t support that, and does not expose an event for that. So you have to roll your own. This is not hard to do. All you have to do is handle the mouse wheel events from the browser using the JavaScript bridge.

On a side note, I did actually find this blog that talks about using UI Automation to handle the scrolling instead. Even though this seems like a cool solution, I was too far down the other line to turn back. So my solution is using the traditional JavaScript way…

Lets start by creating a static class to handle the attached properties. In my case it is called ScrollingHandler. It needs to expose one attached property. It might seem odd, but it will be an integer. Why? Well..because certain controls needs to scroll at a separate speed. A big window should probably scroll fast, while a small ListBox might need slower scrolling. So by setting a “scrolling increment”, I can vary the speed it scrolls at. This integer also doubles as a registration property. Any element with this property should scroll, and should scroll at a certain speed.

Besides exposing the DependencyProperty, it needs to handle the JavaScript mouse wheel events. This is done using three different JavaScript events. Different browsers raise different events, with different information. (It is SO nice to leave the safety of Silverlight, and start having cross browser issues)… Since this will be used continuously, I hook up these handlers in the static constructor.

So the base looks like this:

public static class ScrollingHandler
{
public static DependencyProperty ScrollingIncrementProperty =
DependencyProperty.RegisterAttached("ScrollingIncrement", typeof(int), typeof(ScrollingHandler), new PropertyMetadata(OnScrollingIncrementChanged));

static ScrollingHandler()
{
HtmlPage.Window.AttachEvent("DOMMouseScroll", OnMouseWheel); // Mozilla
HtmlPage.Window.AttachEvent("onmousewheel", OnMouseWheel);
HtmlPage.Document.AttachEvent("onmousewheel", OnMouseWheel); // IE
}
}

To be able to register attached properties, the class also needs to expose GetXXX and SetXXX methods for the property.

public static void SetScrollingIncrement(DependencyObject obj, int increment)
{
obj.SetValue(ScrollingIncrementProperty, increment);
}
public static int GetScrollingIncrement(DependencyObject obj)
{
return (int)obj.GetValue(ScrollingIncrementProperty);
}

 

So…after the base it is time to implement the methods to get the functionality going. As you probably saw in the DependencyProperty registration, I added a callback to handle the change. In this callback, I store the ScrollViewer in a list for later use. There are a few problems with this though. It is possible that the control that has had its ScrollingIncrement set, is not a ScrollViewer. What? Well…If I for example set the ScrollingIncrement on a ListBox, it will not be a ScrollViewer, but it will contain a ScrollViewer that should be scrolled. Unfortunately, the template for the ListBox might not have been loaded at the moment that the property is set. So I can’t get hold of it straight away.

My solution to this is to do the following. If the object is a ScrollViewer, then it is registered straight away. Otherwise, I hook up to the objects MouseMove event, and register it when this is raised. This mean it will be registered as soon as the mouse is moving over it. And by that time it has had its template applied. I handle the event using an anonymous method. But since I only want it to execute once, I need to unregister the handler as soon as it has been called. This is easily done using the following syntax:

static List<ScrollViewer> _scrollViewers = new List<ScrollViewer>();

public static void OnScrollingIncrementChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue != null)
{
if (sender is ScrollViewer)
{
_scrollViewers.Add((ScrollViewer)sender);
}
else
{
UIElement el = (UIElement)sender;
MouseEventHandler handler = null;
handler = delegate {
el.MouseMove -= handler;
ScrollViewer scv = ParseForScrollViewer(sender);
if (scv != null)
SetScrollingIncrement(scv,(int)e.NewValue);
};
el.MouseMove += handler;
}
}
}

But as you can see, I’m doing some other stuff in there. Mainly the call to ParseForScrollViewer. This method goes through the objects children and tries to find a ScrollViewer. If it finds one, it returns it, otherwise it returns null. If it finds a ScrollViewer, its ScrollingIncrement is set. This causes the “changed method” to be called, and the ScrollViewer to be registered. It looks like this

private static ScrollViewer ParseForScrollViewer(DependencyObject obj)
{
if (obj is ScrollViewer)
return obj as ScrollViewer;

for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
{
ScrollViewer scv = ParseForScrollViewer(VisualTreeHelper.GetChild(obj, i));
if (scv != null)
return scv;
}

return null;
}

So…this is all that is needed to register all ScrollViewers in a List<ScrollViewer>. So…now that I have them all in a list, it is time to get going on the handling of the scroll wheel. This is done in the OnMouseWheel method. This is an EventHandler<HtmlEventArgs>. That means that I get event information from the JavaScript in the form of an HtmlEventArgs. From that object, I can get hold of the JavaScript event arguments by looking at the EventObject property. However, that object will work slightly different depending what browser the client uses. That’s why I rolled that in to its own method. So before we can look at the OnMouseWheel method, we have to take a look at that.

This method is called GetScrollDelta. Depending on what properties are available, I can figure out what browser the user is using and customize the code based on this. Mozilla and FireFox happens to scroll “backwards” and very slowly. That is, the “delta” is very low and inverted. So for those browsers, I need to increase the “delta” and invert it

private static double GetScrollDelta(ScriptObject e)
{
double delta = 0;
if (e.GetProperty("detail") != null) // Mozilla and Safari
{
delta = ((double)e.GetProperty("detail"));
delta = delta * -50;
}
else if (e.GetProperty("wheelDelta") != null) // IE and Opera
{
delta = ((double)e.GetProperty("wheelDelta"));
}
return delta;
}

After that sidestep, I can return to the OnMouseWheel method. In this method I start off by getting a ScriptObject from the eventargs and pass it to the GetScrollDelta method to get the scroll delta. After that, I use the VisualTreeHelper class to get all the visual elements that are “under” the mouse pointer at the moment. This is easy. The method is called FindElementsInHostCoordinates, and takes a Point (the location of the mouse) and a UIElement (the element to use as root for the child traversion). In our case, the Point can be determined by using the HtmlEventArgs and the UIElement is the RootVisual for the application. It returns a IEnumerable<UIElement>.

private static void OnMouseWheel(object sender, HtmlEventArgs args)
{
ScriptObject e = args.EventObject;
double delta = GetScrollDelta(e);

IEnumerable<UIElement> elements = VisualTreeHelper.FindElementsInHostCoordinates(new Point(args.OffsetX, args.OffsetY), Application.Current.RootVisual);
}

 

After I get hold of all the element in the specified Point, it is time to start looping through them and see if one of them is in the list of ScrollViewers that I have set up. So I created a foreach loop, going through them. If I find a ScrollViewer, I check to see if it is in the list, if not I keep going. If it is, I calculate the delta to use. I do this by getting the objects ScrollingIncrement. Then I take 11 minus this and divide the mouse wheel delta by this. So, the user defines a number between 0 and 10. The higher the number, the bigger the delta will be. Just to be safe, the delta is checked for infinity and NaN. Then it is finally time to call the ScrollViewer’s ScrollToVerticalOffset method to do the actual scrolling.

foreach (var item in elements)
{
if (item is ScrollViewer)
{
ScrollViewer viewer = item as ScrollViewer;
if (_scrollViewers.Contains(viewer))
{
delta = delta / (11 - GetScrollingIncrement(viewer));
if (!double.IsInfinity(delta) && !double.IsNaN(delta))
{
viewer.ScrollToVerticalOffset(viewer.VerticalOffset - delta);
args.PreventDefault();
}
break;
}
}
}

 

Oh yeah…I also call PreventDefault to tell the browser that I have handled the event and that it doesn’t need to bubble up further.

That’s it. To hook up the handling to the elements, all I have to do is register the namespace and use the following Xaml:

<ScrollViewer scrolling:ScrollingHandler.ScrollingIncrement="10">
<!-- Content -->
</ScrollViewer>

That’s all. Now I have a re-usable scrolling solution that I can add to any future project. If you have any questions or comments, feel free to add a comment or send a message!

Comments (3) -

Hi,

I am using Nikhil Kothari's Silverlight FX behavior:

http://www.nikhilk.net/Silverlight-MouseWheel.aspx

which works very nice for me.

Nikhil's solution looks really sweet. However, this version came to be during a couple of projects that were a little limited as to what we were allowed to add in the form of external stuff. But yeah, his solution looks really nice!

Great solution. Just tried it out and this works excellent.

Comments are closed