Jim McCurdy's Tech Blog

Insights into Software Development and Silverlight

Implement MouseWheel support for Silverlight 3 controls (and for Silverlight 4 Slider and TreeView)

In the upcoming Silverlight 4 release, mousewheel support for controls will be implemented out of the box for the ScrollViewer, DataGrid, and ListBox controls.  However Silverlight 3 still requires you to roll your own mouse wheel support.  And Silverlight 4 does not offer a scrolling solution for Slider and TreeView.

The following MouseWheelProps class provides generic mousewheel support for all Silverlight controls that support the IScrollProvider interface (ScrollViewer, ListBox, DataGrid) or the IRangeValueProvider interface (Slider).  This class implements a single attached property called Enable that can be added directly to a control, or as a style for a control.

Step1: Add the following MouseWheelProps class to your Silverlight project:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Automation.Peers;
using System.Windows.Automation.Provider;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;

namespace ClassLibrary
{
	public static class MouseWheelProps
	{
		// The Enabled attached property
		public static readonly DependencyProperty EnabledProperty = 
			DependencyProperty.RegisterAttached("Enabled", typeof(bool), typeof(MouseWheelProps),
				new PropertyMetadata(false, OnEnabledPropertyChanged));

		public static bool GetEnabled(FrameworkElement element)
		{
			return (bool)element.GetValue(EnabledProperty);
		}

		public static void SetEnabled(FrameworkElement element, bool value)
		{
			element.SetValue(EnabledProperty, value);
		}

		private static void OnEnabledPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
		{
			UIElement element = sender as UIElement;
			if (element == null)
				return;
			bool enabled = (bool)e.NewValue;
			if (enabled)
				element.MouseWheel += OnMouseWheel;
			else
				element.MouseWheel -= OnMouseWheel;
		}

		private static void OnMouseWheel(object sender, MouseWheelEventArgs e)
		{
			if (e.Handled)
				return;

			UIElement element = sender as UIElement;
			if (element == null)
				return;

			AutomationPeer peer = FrameworkElementAutomationPeer.FromElement(element);
			if (peer == null)
				peer = FrameworkElementAutomationPeer.CreatePeerForElement(element);
			if (peer == null)
				return;

			// try to get the scroll provider or the range provider
			IScrollProvider m_ScrollProvider = peer.GetPattern(PatternInterface.Scroll) as IScrollProvider;
			IRangeValueProvider m_RangeValueProvider = null;
			if ((element is Control) && (element as Control).HasFocus())
				m_RangeValueProvider = peer.GetPattern(PatternInterface.RangeValue) as IRangeValueProvider;
			if (m_ScrollProvider == null && m_RangeValueProvider == null)
				return;

			// set scroll amount
			const double kMultiplier = 5.0; // x times the default
			const double kFactor = kMultiplier / (30 * 120);
			double delta = e.Delta;
			delta *= kFactor;
			int direction = Math.Sign(delta);

			bool shiftKey = (Keyboard.Modifiers & ModifierKeys.Shift) != 0;
			bool controlKey = (Keyboard.Modifiers & ModifierKeys.Control) != 0;

			if (m_ScrollProvider != null)
			{
				e.Handled = true;
#if true
				if (m_ScrollProvider.VerticallyScrollable && !controlKey)
				{
					double percent = m_ScrollProvider.VerticalScrollPercent - (delta * m_ScrollProvider.VerticalViewSize);
					if (percent < 0) percent = 0;
					if (percent > 100) percent = 100;
					m_ScrollProvider.SetScrollPercent(m_ScrollProvider.HorizontalScrollPercent, percent);
				}
				else
				if (m_ScrollProvider.HorizontallyScrollable && controlKey)
				{
					double percent = m_ScrollProvider.HorizontalScrollPercent - (delta * m_ScrollProvider.HorizontalViewSize);
					if (percent < 0) percent = 0;
					if (percent > 100) percent = 100;
					m_ScrollProvider.SetScrollPercent(percent, m_ScrollProvider.VerticalScrollPercent);
				}
#else
				ScrollAmount scrollAmount = (direction < 0) ? ScrollAmount.SmallIncrement : ScrollAmount.SmallDecrement;
				if (m_ScrollProvider.VerticallyScrollable && !controlKey)
					m_ScrollProvider.Scroll(ScrollAmount.NoAmount, scrollAmount);
				else
				if (m_ScrollProvider.HorizontallyScrollable && controlKey)
					m_ScrollProvider.Scroll(scrollAmount, ScrollAmount.NoAmount);
#endif
			}

			if (m_RangeValueProvider != null)
			{
				e.Handled = true;
				double newValue = m_RangeValueProvider.Value + (direction < 0 ? -m_RangeValueProvider.LargeChange : m_RangeValueProvider.LargeChange);
				if (newValue >= m_RangeValueProvider.Minimum && newValue <= m_RangeValueProvider.Maximum)
					m_RangeValueProvider.SetValue(newValue);
			}
		}
	}

	internal static class ExtensionMethods
	{
		// Extension for UIElement
		internal static bool HasFocus(this UIElement element)
		{
			if (element == null)
				return false;

			DependencyObject focusedElement = FocusManager.GetFocusedElement() as DependencyObject;
			while (focusedElement != null)
			{
				if (element == focusedElement)
					return true;
				focusedElement = VisualTreeHelper.GetParent(focusedElement);
			}

			return false;
		}
	}
}

Step 2: Add the attached property c:MouseWheelProps.Enabled="True" either directly to a control, or as a Style that can be referenced by a control.

Added directly to a control:

<UserControl x:Class="SilverlightApplication.MainPage"
	xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
	xmlns:c="clr-namespace:ClassLibrary"
	xmlns:DataGrid="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
>
	<Grid>
		<DataGrid:DataGrid c:MouseWheelProps.Enabled="True" />
	</Grid>
</UserContro>

Added as a Style:

<UserControl x:Class="SilverlightApplication.MainPage"
	xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
	xmlns:c="clr-namespace:ClassLibrary"
	xmlns:DataGrid="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
>
	<Grid>
		<Grid.Resources>
			<Style x:Key="CommonDataGrid" TargetType="DataGrid:DataGrid">
				<Setter Property="c:MouseWheelProps.Enabled" Value="True" />
			</Style>
		</Grid.Resources>
		<DataGrid:DataGrid Style="{StaticResource CommonDataGrid}" />
	</Grid>
</UserContro>

 

That's all there is to it!

Comments (16) -

  • David Nelson

    4/7/2010 6:46:10 PM |

    I am not able to make this solution work for a Silverlight 3 DataGrid. First, as noted by a commenter in the Silverlight forums where you posted this solution (forums.silverlight.net/forums/p/38536/289377.aspx), System.Windows.Controls.Control does not have a HasFocus method. I can only surmise that you are using an extension method, so I implemented one. However, the solution still does not work, as the automation peer for the DataGrid returns null for both the Scroll pattern and the RangeValue pattern.

    • Jim McCurdy

      4/7/2010 7:00:07 PM |

      David,

      Sorry about that.  My HasFocus() extension method actually checked for focus among the element's children, so it was a pretty important piece of the solution.  I updated tha code above to included that method and it's dependant methods.

      Jim

    • Phil

      12/9/2010 3:49:45 PM |

      David,

      Did you every find out why you were getting null from automationpeer?  I'm running into this problem as well.

      Thanks,
      Phil

      • Jim McCurdy

        12/10/2010 10:25:51 AM |

        Phil,

        "peer" is set in one of two calls.  Which one fails, and on which Control?  The two calls are made as a way to determine which method (if any) the Control supports.

        Jim

  • David Nelson

    4/8/2010 12:45:28 PM |

    I found another implementation of a HasFocus extension method (below) which works fine. Determining focus is not the main problem; the problem is that the automation peer is returning null for the scroll pattern. Any idea why that would be?

    public static bool HasFocus(this Control aControl, bool aCheckChildren) {
      var oFocused = FocusManager.GetFocusedElement() as DependencyObject;
      if (!aCheckChildren)
        return oFocused == aControl;
      while (oFocused != null) {
        if (oFocused == aControl)
          return true;
        oFocused = VisualTreeHelper.GetParent(oFocused);
      }
      return false;
    }

  • Jim McCurdy

    4/8/2010 3:14:28 PM |

    David,

    I checked the following code with a DataGrid:


    AutomationPeer peer = FrameworkElementAutomationPeer.FromElement(element);
    if (peer == null) // peer is null here for DataGrid
         peer = FrameworkElementAutomationPeer.CreatePeerForElement(element);
    if (peer == null) // peer is a valid object for DataGrid
         return;


    So I am not sure what is different about your implementation....
    Jim

    By the way, I like your HasFocus() method better - a lot faster and simpler

  • Jared

    5/1/2010 1:25:23 PM |

    Thank you for this excellent post.  I was trying to do the same thing but running into alot of problems.  You saved me HOURS!

  • Baumann Benjamin

    6/17/2010 12:13:03 PM |

    It works "out of the box" with my datagrids.
    Thank you, you saved me a lot of time.

  • Social Media Packages

    8/30/2010 6:22:40 AM |

    Yeap gotta agree, works "out of the box"

    Well done Jim. Top marks.

  • Grants for Single Mothers

    9/14/2010 10:58:35 PM |

    It took me a while to compile your code successfully. Its my mistake, I had some stupid syntax error. Anyways, it works like a charm. Thanks Jim!

  • Nishit

    9/16/2010 8:29:08 AM |

    Hi,
    You have used, c:MouseWheelProps.Enabled="True" here but not included any namespace in the header for "c" used here.  Can u please write entire code i m a novice to Silverlight.  

    • jim mccurdy

      9/16/2010 9:32:37 PM |

      Sorry.  I fixed the namespace references above:

          Removed xmlns:app="clr-namespace:SilverlightApplication"
          Added xmlns:c="clr-namespace:ClassLibrary"

  • Lynda

    11/30/2010 12:27:28 PM |

    Thank you, you saved me a lot of time.

  • Ryan

    4/13/2011 12:02:22 PM |

    Hi Jim,

    Have you encountered a problem with silverlight 3 datagrids when grouping is used and you've collapsed one of the groups? Specifically, I'm collapsing the top group. Essentially, I can scroll down with a down arrow or the mouse wheel, but when I scroll up, I get the following error.

    System.ArgumentOutOfRangeException was unhandled by user code
      Message=Index must be within the bounds of the List.
    Parameter name: index
      StackTrace:
           at System.ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument argument, ExceptionResource resource)
           at System.Collections.Generic.List`1.Insert(Int32 index, T item)
           at System.Windows.Controls.DataGridDisplayData.LoadScrollingSlot(Int32 slot, UIElement element, Boolean updateSlotInformation)
           at System.Windows.Controls.DataGrid.InsertDisplayedElement(Int32 slot, UIElement element, Boolean wasNewlyAdded, Boolean updateSlotInformation)
           at System.Windows.Controls.DataGrid.InsertDisplayedElement(Int32 slot, Boolean updateSlotInformation)
           at System.Windows.Controls.DataGrid.GetExactSlotElementHeight(Int32 slot)
           at System.Windows.Controls.DataGrid.UpdateDisplayedRows(Int32 newFirstDisplayedSlot, Double displayHeight)
           at System.Windows.Controls.DataGrid.ScrollSlotIntoView(Int32 slot, Boolean scrolledHorizontally)
           at System.Windows.Controls.DataGrid.ProcessVerticalScroll(ScrollEventType scrollEventType)
           at System.Windows.Automation.Peers.DataGridAutomationPeer.System.Windows.Automation.Provider.IScrollProvider.Scroll(ScrollAmount horizontalAmount, ScrollAmount verticalAmount)

    I could handle the exception, but this still won't get my datagrid to scroll.

    Thanks,
    Ryan

  • Gopi

    7/25/2011 11:30:33 PM |

    Hi,

    I have a datagrid in my program which has a template column and some other regular columns. I used the template column ,it has some controls textbox and combobox.controls are dynamically binded and selected index of the combobox is set according to the data in the service.Everything works fine but problem comes when i scroll the datagrid ,all combobox selected item got changed,it seems loading row  event is getting fired when i scroll the datagrid but cant able to resolve it.

Comments are closed