Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Automatic Scrolling During Drag operation

0.00/5 (No votes)
11 Jul 2013 2  
A way to achieve automatic scrolling during a drag operation

Introduction 

It is often desirable that during a drag operation if mouse moves to an edge, the underlying view scrolls to bring 'more' into view. This article introduces you to a way to achieve such a scrolling automatically during a drag operation inside a WPF control.

The presented solution can be easily enabled on any WPF control(for e.g. Grid, StackPanel, WrapPanel or even an ItemsControl) be it placed inside a ScrollViewer or contains a ScrollViewer inside its template (for e.g. TreeView, ListBox etc.). What the solution is not about though is how to enable and handle drag and drop operations.

WPF is known to be highly flexible. For most cases this is WPF's biggest strength but when you try to write a generic piece of utility code you soon discover that this very strength makes it quite hard to cover all the possible cases. This article does not claim to work in absolutely all possible scenarios but I have nevertheless tried to attain this goal while trying to keep the solution as generic as possible. I am confident that it will work seamlessly for vast majority of cases.

Other Features

Reusable

Attached properties are one of the most powerful feature of WPF.
Apart from associating additional state with a DependencyObject, they can also be used to attach new behavior while remaining XAML friendly.
I use this approach in the solution to keep it reusable and distributable in a library.
The attached demo application contains a class named as AutoDragScrollingProvider and presents an attached property EnableAutomaticScrolling to easily enable automatic scrolling on any UIElement it gets attached to.  

Smooth Scrolling 

You may be aware that WPF supports two modes of scrolling:
  • Pixel based scrolling  
  • and 'logical' scrolling where scrolling is performed logically which in most cases mean item by item scrolling. For e.g. ListBox uses item by item scrolling by default 

Pixel based scrolling is quite smooth and is in line with what those of us coming from WinForms world expect. Logical scrolling on the other hand appears to be a bit jerky to the end user especially in case of item by item scrolling as an entire item moves out and new one comes in. 

To enable logical scrolling, ScrollViewer.CanContentScroll property must be set to true and one of the following conditions must be met:

  • Either ScrollViewer.Content implements IScrollInfo interface
  • or ScrollViewer is part of a template and it contains ItemsPresenter in its child visual tree and ItemsPresenter's child (i.e. panel contained in ItemsControl.ItemsPanel template) implements IScrollInfo interface 
For instance, VirtualizingStackPanel and StackPanel implements IScrollInfo and if above scenario is met, ScrollViewer will scroll logically as dictated by these panels.

While dragging it is often desirable to control the speed of scrolling by 'pressure' of the mouse i.e nearer the mouse to the edge (giving impression of a 'hard press') faster should be the scrolling. For the purpose of this article (and attached demo) I use words 'smooth scrolling' in this context rather than pixel based scrolling as described above. In this context 'smooth scrolling' does not affect the ability to perform item by item scrolling but just that scrolling pace is more controllable by the user and hence provides more pleasant user experience.  

AutoDragScrollingProvider makes sure that pixel based scrolling is always smooth (in just described context) while logical scrolling may or may not be smooth depending on specific implementation of IScrollInfo. To support smooth scrolling, IScrollInfo implementation must support fractional changes in scroll offsets.
For instance StackPanel supports smooth scrolling in the direction opposite to its orientation while in the direction of its orientation it does not support it.
Case of VirtualizingStackPanel is a bit more complex than that. When it is not hosted inside an ItemsControl (as dictated by ItemsControl.ItemsPanel) it behaves like a StackPanel but otherwise it supports smooth scrolling irrespective of its orientation.
It must be remembered however, that if CanContentScroll property is false or none of the necessary condition is met, ScrollViewer uses ScrollContentPresenter (which itself implements IScrollInfo) to provide pixel based scrolling by default.
Another point to remember is that any IScrollInfo implementation can be used to perform non-smooth scrolling by using its line scrolling methods. 

AutoDragScrollingProvider attempts to make use of 'smooth scrolling' wherever possible as it makes for a more pleasant user experience. It automatically takes care of different cases arising due to StackPanel and VirtualizingStackPanel as far as support for smooth scrolling is concerned and presents a set of attached properties to enforce 'non-smooth' line scrolling for those IScrollInfo custom implementations which don't support smooth scrolling. 

Scrolling irrespective of origin of drag operation

In certain drag and drop operations it is often required that drag starts from a particular control and terminates in another. For e.g. Moving an item from one list to another by dragging it from former and dropping it on later. In other cases drag and drop is restricted to a single control (for e.g. re-ordering of rows by dragging).

AutoDragScrollingProvider automatically works on any control that has its AllowDrop property set to true (this is necessary to enable the dragging itself) and off-course on which EnableAutomaticScrolling is set to true. It is not dependent on whether  drag started from underlying control or not. Hence, it is able to support both the above scenarios automatically.

Using the code

A demo application is attached with this article. It exhibits how AutoDragScrollingProvider can be used in different scenarios.

To enable automatic scrolling simply set AutoDragScrollingProvider.EnableAutomaticScrolling attached property to true and remember to set AllowDrop to true as well. For e.g.

<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
  <WrapPanel AllowDrop="True" local:AutoDragScrollingProvider.EnableAutomaticScrolling="True">
    ...child elements...
  <WrapPanel>
<ScrollViewer>

If you happen to use a custom implementation of IScrollInfo and you know it does not support smooth scrolling (i.e. fractional scroll offset changes) in a particular direction you can set AutoDragScrollingProvider.UseLineScrollingForHorizontalChange or AutoDragScrollingProvider.UseLineScrollingForVerticalChange attached properties to true to enforce use of 'non-smooth' line scrolling in respective directions. 

Point of Interest

Handling of  IScrollInfo in a generic fashion can be a bit tricky. IScrollInfo is an interface and hence a lot depends on its specific implementation. Some implementations of it present their scroll properties (offsets, view port dimensions etc.) in pixels, some in logical units, and still some in logical units for one scroll direction while pixels for another. 

AutoDragScrollingProvider is able to deal with this in a consistent manner by calculating average width (or height) per item in pixels for logical scrolling cases (for pixel based scrolling average always comes out to be 1 and as such does not affect subsequent calculation) and then using the pixel units to dictate the amount of scrolling required. 

How it works 

  • When the attached property is set to true, AutoDragScrollingProvider subscribes PreviewDragEnter and PreviewDragOver events on the UIElement it gets attached to
  • In the event handler for these events, it searches for the relevant IScrollInfo in force (which by default is ScrollContentPresenter) and caches it for subsequent uses 
  • Next it gets the position of the mouse with in the control represented by cached IScrollInfo
    UIElement scrollable = _scrollInfo as UIElement;
    var mousePos = e.GetPosition(scrollable);
    
  • Now it checks if the mouse is with in 20 pixels from any edge and if it is it calculates average width or average height (as the case may be). If the view-port dimension is in pixels this average always comes out to be 1, otherwise this value can be treated as average per item in pixels.
    var avgWidth = scrollable.RenderSize.Width / _scrollInfo.ViewportWidth; //translate to pixels
    
  • Next it calculates the amount by which scroll offset needs to change and then changes the offset to cause scrolling. Nearer the mouse to the edge, greater is the delta. As you may guess delta in most cases is a fractional value.
    var delta = (mousePos.X - 20) / avgWidth; //translate back to original unit
    _scrollInfo.SetHorizontalOffset(_scrollInfo.HorizontalOffset + delta);
    
  • For cases where line scrolling is enforced using the other two attached properties, well it simply causes line scrolling instead of delta based scrolling (Note: Please see the Updates section). For e.g.
    _scrollInfo.LineLeft();
    

Updates  

Update 1  

Replacing UseLineScrollingForHorizontalChange and UseLineScrollingForVerticalChange attached properties with LineSizeForHorizontalChange and LineSizeForVerticalChange attached properties. Demo Application V2 contains the update for AutoDragScrollingProvider class. Both of these properties are of type double with default value of double.NaN.  

When any of these properties is provided with a valid value, scrolling in the respective direction uses line scrolling as earlier but now, just like the default 'smooth scrolling' case, it is dependent on the calculated delta and thus allow user to control the scrolling speed using 'mouse pressure'. Nearer the mouse to the edge, faster is the scrolling. 

For StackPanel and VirtualizingStackPanel these properties are automatically taken care of. For those custom implementations of IScrollInfo which do not behave well with default smooth scrolling, these properties can be set to instruct line scrolling while at the same time giving the impression of smooth scrolling. As explained earlier ability to control speed of the scroll makes for a more pleasant user experience. If you try Demo Application V1 you can see that in case of line scrolling, scrolling happens just too fast to be useful. Contrast the same case with Demo Application V2 where scrolling speed can be controlled. 

How is this achieved 

Please recall from previous discussion that the calculated delta is almost always fractional. Some implementations of IScrollInfo do not support 'smooth scrolling' because they do not support fractional scroll offset changes. So the solution is to accumulate the calculated delta till it has exceeded the 'amount' of scroll affected by the line scroll (or in other words 'line size') and only then cause the line scroll. Thus we can simulate smooth scrolling even when line scrolling is required. It must be remembered though that the line size itself must be in units understood by the IScrollInfo implementation. For instance for StackPanel it is 1.0, the minimal offset change it is able to support. 

var delta = (mousePos.X - 20) / avgWidth; //translate back to original unit
var lineSize = GetLineSizeForHorizontalChange(scrollable);
if (!double.IsNaN(lineSize))
{
    _totalHorizontalDelta += delta;
    if ((0 - _totalHorizontalDelta) >= lineSize) //since delta is negative here
    {
        _scrollInfo.LineLeft();
        _totalHorizontalDelta = 0d;
    }
}
else
{
    _scrollInfo.SetHorizontalOffset(_scrollInfo.HorizontalOffset + delta);
}

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here