Scrolling while dragging and dropping (WPF)
Asked Answered
L

3

15

Okay guys, I have been scratching my head like mad over this issue and have spent a good few hours trying to research how it works but I am yet to find an answer, if you wish to see any of my SRC feel free to ask about it and I will see if I can help.

Basically the issue I am having is that I have a TreeView of folders in my application i.e.:

Catalog

  Brands
    Nike
    Adidas
    Lactose

  Styles
    Sandles
    Trainers
    Boots

The issue that I am trying to fix is that when I drag a folder around (This is handled in my DragDropManager class), I am unable to scroll up or down(simply displays a lovely stop sign). I am also unable to find a scroller actually within the TreeView, so I am unsure how it is being generated (This is not my own software, I have recently started working for a company so I am not familiar with the code and no one else seems to know.)

This is a problem if I want to move something from the very top to the very bottom.

The scrolling works fine on its own without the dragging being done.

If anyone wishes to see any part of my code feel free to ask as I am unsure what to actually show you guys.

I have read through a good few articles and am just left scratching my head.

Lennox answered 24/5, 2012 at 8:13 Comment(0)
R
31

I have created an attached property for achieving this behavior, have a look at my post here -

Attached Behavior for auto scrolling containers while doing Drag & Drop

Main logic is something like this -

private static void OnContainerPreviewDragOver(object sender, DragEventArgs e)
{
    FrameworkElement container = sender as FrameworkElement;

    if (container == null) { return; }

    ScrollViewer scrollViewer = GetFirstVisualChild<ScrollViewer>(container);

    if (scrollViewer == null) { return; }

    double tolerance = 60;
    double verticalPos = e.GetPosition(container).Y;
    double offset = 20;

    if (verticalPos < tolerance) // Top of visible list? 
    {
        //Scroll up
        scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset - offset);
    }
    else if (verticalPos > container.ActualHeight - tolerance) //Bottom of visible list? 
    {
        //Scroll down
        scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset + offset);     
    }
}

Similar questions on SO (although they are mostly for ListBox/ListView but should work for TreeView too) -

WPF Listbox auto scroll while dragging

WPF ListView Databound Drag/Drop Auto Scroll

WPF Drag-to-scroll doesn't work correctly

Roughandtumble answered 29/5, 2012 at 9:7 Comment(8)
I've been trying to use this on my listbox, but it doesn't work if the items in the listbox can't be dropped on. I have mixed items in the listbox, some can be dropped on, some can't. If the ones near the top (or bottom) can't receive drops the scrolling doesn't activate. Any ideas on a way around that?Disorganization
@Lutz that's very interesting, never came across such scenario so can't say much. I can think of 1. Add some dummy items between current items (with small height etc. so they don't look odd or waste space), make them droppable so that PreviewDragOver is always called for them. But you will have to handle drop etc. 2. Make all items droppable and handle validation after drop. 3. Try to find if some other event is fired for these non-droppable items.Roughandtumble
@nikotromus can you please elaborate what doesn't work? and your scenario etc.!Roughandtumble
@Roughandtumble - I have added your code exactly as you have specified. but, when I add the line 'WpfExtensions:DragDropExtension.ScrollOnDragDrop="True"' to my ListView, it gives me the intelisense error of: "The name DragDropExtension does not exist in the namespace 'clr-namespace:WpfExtensions' "Seethe
@Seethe ok, have you used the attached file from my blog or copied the code? I just realized that attached file is an old version with a different name - ListBoxExtension inside another namespace. Just try copying code from blog.Roughandtumble
@Roughandtumble I am using your example to implement drag and drop on multiple views, however in the extension class, when i execute GetFirstVisualChild, with one view it comes back with a type "System.Windows,Controls.Border" with another comes back with type "System.Windows.Controls.Grid". the View that comes back with the Grid is not working while the one coming back as border is. How can i adjust the method to make it always come back with the border dependency?Casia
This extension works also for ScrollViewers. But you have to wrap the ScrollViewer in a Border and add the extension to the border instead of the ScrollViewer.Launcelot
Thanks @jor. I had following comment on my blog regarding ScrollViewer: Made a slight change to allow working directly on the ScrollViewer. #if true ScrollViewer scrollViewer = container as ScrollViewer; if (scrollViewer == null) GetFirstVisualChild<ScrollViewer>(container); #else ScrollViewer scrollViewer = GetFirstVisualChild<ScrollViewer>(container); #endifRoughandtumble
C
3

I know this question really old, but here is the MVVM way as attached property:

    using System.Windows;
    using System.Windows.Controls;

    namespace AndroidCtrlUI.XTools.Behaviors
    {
        ///<summary>
        /// TreeItemAttach 
        ///<para/> TreeViewItem
        ///</summary>
        public sealed class TreeItemAttach
        {
            #region BringIntoView

            ///<summary>
            /// DependencyProperty
            ///</summary>
            public static readonly DependencyProperty BringIntoViewProperty = DependencyProperty.RegisterAttached("BringIntoView", typeof(bool), typeof(TreeItemAttach), new UIPropertyMetadata(false, (s, e) =>
            {
                if ((bool)e.NewValue != (bool)e.OldValue && s is TreeViewItem t)
                {
                    if ((bool)e.NewValue)
                    {
                        t.Selected += BringIntoView;
                    }
                    else
                    {
                        t.Selected -= BringIntoView;
                    }
                }
            }));

            ///<summary>
            /// Get
            ///</summary>
            ///<param name="target">DependencyObject</param>
            ///<returns>ICommand</returns>
            public static bool GetBringIntoView(DependencyObject target)
            {
                return (bool)target.GetValue(BringIntoViewProperty);
            }

            ///<summary>
            /// Set
            ///</summary>
            ///<param name="target">DependencyObject</param>
            ///<param name="value">ICommand</param>
            public static void SetBringIntoView(DependencyObject target, bool value)
            {
                target.SetValue(BringIntoViewProperty, value);
            }

            private static void BringIntoView(object sender, RoutedEventArgs e)
            {
                if (e.Source is TreeViewItem s)
                {
                    double h = s.ActualHeight;
                    if (s.IsExpanded && s.Items.Count > 0)
                    {
                        h = s.ActualHeight / TreeWalker(s);
                    }
                    s.BringIntoView(new Rect(0, h * -1, s.ActualWidth, h * 2.5));
                }
            }

            private static long TreeWalker(TreeViewItem item)
            {
                long c = item.Items.Count;
                foreach (object i in item.Items)
                {
                    if (i != null && item.ItemContainerGenerator.ContainerFromItem(i) is TreeViewItem t && t.IsExpanded && t.Items.Count > 0)
                    {
                        c += TreeWalker(t);
                    }
                }
                return c;
            }
            #endregion
        }
    }

And it can be used like:

<Style x:Key="TreeViewItemStyle" TargetType="{x:Type TreeViewItem}">
    <Setter Property="tool:TreeItemAttach.BringIntoView" Value="True"/>
</Style>
Cemetery answered 3/5, 2019 at 14:33 Comment(0)
P
1

Based on @akjoshi's answer, but I wanted variable speed while dropping like this: enter image description here

My PreviewDragOver handler is this:

private static void container_PreviewDragOver(object sender, DragEventArgs e)
{
    if (!(sender is FrameworkElement container))
        return;

    var scrollViewer = findChildOfType<ScrollViewer>(container);
    if (scrollViewer == null)
        return;

    const double heightOfAutoScrollZone = 25;
    double mouseYRelativeToContainer = e.GetPosition(container).Y;

    if (mouseYRelativeToContainer < heightOfAutoScrollZone)
    {
        double offsetChange = heightOfAutoScrollZone - mouseYRelativeToContainer;
        scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset - offsetChange);
    }
    else if (mouseYRelativeToContainer > container.ActualHeight - heightOfAutoScrollZone)
    {
        double offsetChange = mouseYRelativeToContainer - (container.ActualHeight - heightOfAutoScrollZone);
        scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset + offsetChange);
    }
}

Full code and sample on github.

Pigfish answered 1/6, 2023 at 19:4 Comment(1)
Works great in my DataGrid!Roast

© 2022 - 2024 — McMap. All rights reserved.