A Touch-Scrolling Attached Behaviour for ScrollViewer
I'm working on a WPF application here at work that will run on a touch-screen system, but one that's still running Windows XP. That means that all "touch" events are really just "mouse left button" events.
Nowadays when a user sees a list of things on a touch screen, they intuitively think they can scroll the list by dragging it up or down, just like they would on a phone. That, of course, doesn't work by default in WPF applications, so I wrote an attached behaviour to make it work.

I don't think I'd call this "best practice" - there are, after all, some "touch specific" events in WPF that I should probably be handling as well as the mouse ones - but it works just fine.
First, the usage:
<ScrollViewer my:TouchScrolling.IsEnabled="True" />
(Or put the attached behaviour in a style that applies to all ScrollViewer instances, as I have.)
And secondly, the class itself:
Update - we're now capturing the mouse, which makes the scrolling work even if your underlying list contains clickable elements like buttons. You won't accidentally click things when you lift the mouse after scrolling.
public class TouchScrolling : DependencyObject
{
public static bool GetIsEnabled(DependencyObject obj)
{
return (bool)obj.GetValue(IsEnabledProperty);
}
public static void SetIsEnabled(DependencyObject obj, bool value)
{
obj.SetValue(IsEnabledProperty, value);
}
public bool IsEnabled
{
get { return (bool)GetValue(IsEnabledProperty); }
set { SetValue(IsEnabledProperty, value); }
}
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.RegisterAttached("IsEnabled", typeof(bool), typeof(TouchScrolling), new UIPropertyMetadata(false, IsEnabledChanged));
static Dictionary<object, MouseCapture> _captures = new Dictionary<object, MouseCapture>();
static void IsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var target = d as ScrollViewer;
if (target == null) return;
if ((bool)e.NewValue)
{
target.Loaded += target_Loaded;
}
else
{
target_Unloaded(target, new RoutedEventArgs());
}
}
static void target_Unloaded(object sender, RoutedEventArgs e)
{
System.Diagnostics.Debug.WriteLine("Target Unloaded");
var target = sender as ScrollViewer;
if (target == null) return;
_captures.Remove(sender);
target.Loaded -= target_Loaded;
target.Unloaded -= target_Unloaded;
target.PreviewMouseLeftButtonDown -= target_PreviewMouseLeftButtonDown;
target.PreviewMouseMove -= target_PreviewMouseMove;
target.PreviewMouseLeftButtonUp -= target_PreviewMouseLeftButtonUp;
}
static void target_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var target = sender as ScrollViewer;
if (target == null) return;
_captures[sender] = new MouseCapture
{
VerticalOffset = target.VerticalOffset,
Point = e.GetPosition(target),
};
}
static void target_Loaded(object sender, RoutedEventArgs e)
{
var target = sender as ScrollViewer;
if (target == null) return;
System.Diagnostics.Debug.WriteLine("Target Loaded");
target.Unloaded += target_Unloaded;
target.PreviewMouseLeftButtonDown += target_PreviewMouseLeftButtonDown;
target.PreviewMouseMove += target_PreviewMouseMove;
target.PreviewMouseLeftButtonUp += target_PreviewMouseLeftButtonUp;
}
static void target_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
var target = sender as ScrollViewer;
if (target == null) return;
target.ReleaseMouseCapture();
}
static void target_PreviewMouseMove(object sender, MouseEventArgs e)
{
if (!_captures.ContainsKey(sender)) return;
if (e.LeftButton != MouseButtonState.Pressed)
{
_captures.Remove(sender);
return;
}
var target = sender as ScrollViewer;
if (target == null) return;
var capture = _captures[sender];
var point = e.GetPosition(target);
var dy = point.Y - capture.Point.Y;
if (Math.Abs(dy) > 5)
{
target.CaptureMouse();
}
target.ScrollToVerticalOffset(capture.VerticalOffset - dy);
}
internal class MouseCapture
{
public Double VerticalOffset { get; set; }
public Point Point { get; set; }
}
}
There are some quirks here. For example, I noticed that the ScrollViewer was actually being loaded, unloaded and loaded again when the content was shown. That meant that I couldn't just hook up the events in the IsEnabledChanged method and unhook them in the target_Unloaded event handler, because they were being unhooked immediately. Instead, I've had to hook them up in a handler for the Loaded event, which in turn never gets unhooked. That means that there's something of a "memory leak" in there, but it's one I'm prepared to live with. I'm sure I could work around this with weak events, but I've not done much with those and didn't want to do the learnin' this morning. :)
Trackbacks
- A Touch-Scrolling Attached Behaviour for ScrollViewer « Mas-Tool's Favorites | http://mas-tool.com/?p=4538
- Dew Drop – October 12, 2011 | Alvin Ashcraft's Morning Dew | http://www.alvinashcraft.com/2011/10/12/dew-drop-october-12-2011/
Comments
No comments yet. Be the first!