Introduction

This article describes a ListView derived custom control within which users can drag to scroll its content. It is designed to make user interfaces more intuitive, and it could be a good fit for some touch screen applications.

The demo EXE requires either Windows XP or Windows Vista with .NET 3.5 SP1 installed, and the source code was compiled and tested against Visual Studio 2008 SP1.

Background

Here is a brief description of the design goals for DraggableListView:

Hide horizontal and vertical scrollbars, and add new functionality to allow scrolling by tracking the PreviewMouseDown, PreviewMouseMove, and PreviewMouseUp mouse events.

Add four new Dependency Properties VerticalCurrentPage, VerticalTotalPage, HorizontalCurrentPage, and HorizontalTotalPage to help tracking while scrolling. Since there is no horizontal or vertical scrollbars, it may be hard for users to tell the whereabouts without these four properties.

Add algorithm to calculate dragging speed from the PreviewMouseDown event to the PreviewMouseUp event. Based on that speed, scrolling after the MouseUp event should be either stopping immediately, gradually slowing down, or continuing to scroll until it reaches the other end.

While scrolling after the MouseUp event, the user should be able to stop scrolling immediately with a single mouse click.

Select an item by double-clicking instead of single-clicking; this will prevent selection changes while scrolling.

Using the Code

To use the control, you can simply copy the file DraggableListView.cs into your own project and include the following lines:

Also, you can check the demo source code on how to use the control and set its properties.

How it Works

Here, I will briefly describe how this control is actually coded:

1. Access the ScrollViewer inside ListView

In order to programmatically scroll the content of a ListView, we need to have a reference to the ScrollViewer object inside the ListView. I did this in the function OnApplyTemplate() by calling GetVisualChild(), as follows:

The variables mouseDownPoint and mouseDownTime are initially saved when the user starts to drag the mouse, and move point 1 to move point 3 are the last three points during a mouse drag movement. If a mouse drag on the control is like a straight line, we can calculate the scrolling speed simply using mouseDownPoint and mouseMovePoint3. But if the user zigzagged on the control, the best estimate for the scrolling speed is to use mouseMovePoint1 and mouseMovePoint3.

3. Stop scrolling immediately with a mouse click

Scrolling after the MouseUp event is implemented using a Storyboard object. I added logic in the PreviewMouseDown event to check whether the Storyboard object is still active. If it is, I will first pause the object, save the ScrollViewer's current horizontal and vertical offsets, then clear out the value provided by the animation, and finally, set back the horizontal and vertical offsets.

You can check the relevant code here:

...
if (storyboard != null)
{
// Pause any storyboard if still active
if (storyboard.GetCurrentState(this) == ClockState.Active)
{
storyboard.Pause(this);
}
// Save the current horizontal and vertical offset
double tempHorizontalOffset = listViewScrollViewer.HorizontalOffset;
double tempVerticalOffset = listViewScrollViewer.VerticalOffset;
// Clear out the value provided by the animation
this.BeginAnimation(DraggableListView.ScrollViewerHorizontalOffsetProperty, null);
// Set the current horizontal offset back
listViewScrollViewer.ScrollToHorizontalOffset(tempHorizontalOffset);
// Clear out the value provided by the animation
this.BeginAnimation(DraggableListView.ScrollViewerVerticalOffsetProperty, null);
// Set the current vertical offset back
listViewScrollViewer.ScrollToVerticalOffset(tempVerticalOffset);
storyboard = null;
}
...

4. Select an item by double-click

In order to achieve selecting/unselecting an item by double-click instead of single-click, we need to hide the SelectionChanged event for single mouse click from DraggableListView and only fire the event caused by other situations like double mouse click, etc.

I use the local variables selectedItemPrivate and selectedItemsPrivate to keep a copy of the SelectedItem and SelectedItems properties in case they get out of synchronization. I also use a few other boolean variables to keep track of the different situations.

And finally, I synchronize the local copy of selectedItemPrivate and selectedItemsPrivate back to the SelectedItem and SelectedItems properties in the functions draggableListView_MouseButtonDown(), OnPreviewMouseMove(), and OnPreviewMouseUp(), with the following lines of code.

5. Logic when dragging reaches an edge

When dragging reaches either edge, the content will stop moving while the user continues to drag the mouse. This will cause the previously saved variable initialOffset to be invalid, and we need logic in the function OnPreviewMouseMove() to detect whenever we reach an edge and re-save all the necessary information.

Limitation

Quote from MSDN: "If you require physical scrolling instead of logical scrolling... set its CanContentScroll property to false", and so I set CanContentScroll to false in the function OnApplyTemplate(). But setting CanContentScroll to false also turns off virtualizing, which makes this class less useful if you have lots of rows to display. Also because of this, you will notice there is a pause when you switch to the "Editable ListView Sample" for the first time.

Feel free to use the DraggableListView in your WPF applications. I hope you find WPF as cool as I do.

Comments and Discussions

I'm trying to use this draggablelistview with some buttons inside but when i click on a button it is not fired.
Is there any way to set autoscroll false while mouse is over a button or any other control inside listview ?

Yes i know that yoru sample uses double click to select a row, but i don't want to select that row.
I have some buttons in a row and i want to be able to click on it. RIght now i can't because when i click once it will scroll, doesn't matter if there is a button or anything else.

I implemented double-click for selection and single-click and move for dragging, because it is easier to do. If you want to implement single-click for selection and single-click and move for dragging, it surely is possible. Just a bit harder. Basically, you have to implement the logic to figure out how much of a move with a single-click becomes a drag.

When I wrote this article a few years ago, I had access to a touch screen PC, and I was able to test every change I made. Right now, I do not have that. So, sorry, I cannot update this control any more.

thanks for this great component, very very good work;)
I'd would like to change the color of a selected item, blue is too boring
I've tried to change it with ControlTemplate.Triggers, but it doesn't work.
Need some help please.

Really nice work, I am now struggling with the similiar issue on a TableLayoutPanel, I guess I can get some ideas from your article.
Btw, when I test your demo, it seems that tabpage control doesn't do resizing while form is being resized.
Anyway, I will dig into your code, and "steal" some good points from your work, cheer

"Bd" is the name of the Border control defined in the default control template for ListView, and it is the parent of ScrollViewer. Since ScrollViewer itself does not have a name, I have to use "Bd" to get a reference of the ScrollViewer.

If you start scrolling up and then continue to scroll down back to the "beginMousePoint", it should be considered as a scroll and not changing item selection, right? So, I think there must be some logic in OnPreviewMouseMove() too.

A couple more modifications over and above what merkuriev_sergey wrote to make single click work better. I really like this control. Is there anything out-of-the-box to do this yet?
These changes let you click to stop the animation without doing a selection (didStopAnimation flag). Also prevents from dragging back to nearly the exact same spot and make a selection (wasEverScrolling flag).

Thank you for the excellently documented code. This functionality is just what I needed for my application. Question: is there a XAML attribute that will change the selection to happen on single-clicks instead of double clicks, or do I have to edit the OnSelectionChanged method in the DraggableListView.cs file? I tried deleting this

if (isMouseDown == true && isMouseDoubleClick == false)
{
// This may be one of the following cases:
// 1) mouse single click
// 2) mouse drag from one item to next
mayBeOutOfSync = true;
e.Handled = true;
}

but the result is that items get selected when you don't want them to (i.e. when you're just scrolling). In short, I'm looking for is a way to only allow items to be selected when you click them, not on mouseDown.