How to reorder TreeViewItem by drag&drop in TreeView using interactivity.behavior class in WPF

BabyHai 61 Reputation points
2023-09-08T10:49:00.42+00:00

I have implemented the Drag&Drop from a ListBox to a TreeView, as displayed below.

Drag&Drop from ListBox to TreeView Now I want, for example, after dropping items to TreeView, I can drag ListBoxItem2 and drop it in the TreeViewItem1 and vice versa for other two items. I am using Behavior class from System.Windows.Interactivity library to realize such feature.

Here is the code

XAML:

<Window x:Class="TreeviewExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
        xmlns:local="clr-namespace:TreeviewExample"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:MainViewModel />
    </Window.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <GroupBox Header="Features" Grid.Column="0" Grid.Row="0" Grid.RowSpan="2" Margin="10,10,10,10">
            <!-- Features ListBox -->
            <ListBox x:Name="featureListBox"
         AllowDrop="True"
         ItemsSource="{Binding ListBoxSource}" Margin="5,10,5,10">
                <i:Interaction.Behaviors>
                    <local:ListBoxDragDropBehavior />
                </i:Interaction.Behaviors>
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <Grid Margin="0,2">
                            <TextBlock Text="{Binding ItemName}" />
                        </Grid>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
        </GroupBox>

        <GroupBox Header="Template" Grid.Column="1" Grid.Row="0" Grid.RowSpan="2" Margin="10,10,10,10">
            <Grid>
                <TreeView Name="templateTreeview"
       ItemsSource="{Binding TreeViewSource}"
       FontSize="14"
       AllowDrop="True" Margin="5,5,5,10">
                    <i:Interaction.Behaviors>
                        <local:TreeViewDragDropBehavior />
                    </i:Interaction.Behaviors>
                    <TreeView.ItemContainerStyle>
                        <Style TargetType="{x:Type TreeViewItem}">
                            <Setter Property="IsExpanded" Value="True" />
                        </Style>
                    </TreeView.ItemContainerStyle>
                    <TreeView.Resources>
                        <HierarchicalDataTemplate DataType="{x:Type local:ViewItem}" ItemsSource="{Binding Path=ViewItems}">
                            <StackPanel Orientation="Horizontal">
                                <TextBlock Text="{Binding Path=ItemName}" Margin="0,0,10,0" />
                            </StackPanel>
                        </HierarchicalDataTemplate>
                    </TreeView.Resources>
                </TreeView>
            </Grid>
        </GroupBox>
    </Grid>
</Window>

ViewModel:

using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interactivity;
using System.Windows.Media;

namespace TreeviewExample
{
    public class MainViewModel
    {
        public ObservableCollection<ViewItem> ListBoxSource { get; set; }
        public ObservableCollection<ViewItem> TreeViewSource { get; set; }

        public MainViewModel()
        {
            ListBoxSource = new ObservableCollection<ViewItem>();
            TreeViewSource = new ObservableCollection<ViewItem>();

            ListBoxSource.Add(new ViewItem("ListBoxItem1"));
            ListBoxSource.Add(new ViewItem("ListBoxItem2"));

            ViewItem defalutView = new ViewItem("Root", 1);
            defalutView.ViewItems.Add(new ViewItem("TreeViewItem1"));
            defalutView.ViewItems.Add(new ViewItem("TreeViewItem2"));
            TreeViewSource.Add(defalutView);
        }
    }   
}

Model:

using System.Collections.ObjectModel;

namespace TreeviewExample
{
    public class ViewItem
    {
        public string ItemName { get; set; }
        public ObservableCollection<ViewItem> ViewItems { get; } = new ObservableCollection<ViewItem>();
        public int IsVisible { get; set; }

        public ViewItem()
        { }

        public ViewItem(string name, int IsVisible = 0)
        {
            this.ItemName = name;
            this.IsVisible = IsVisible;
        }
    }
}

ListBoxDrapDropBehavior:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interactivity;
using System.Windows.Media;

namespace TreeviewExample
{
    public class ListBoxDragDropBehavior : Behavior<UIElement>
    {
        protected override void OnAttached()
        {
            AssociatedObject.PreviewMouseLeftButtonDown += PreviewMouseLeftButtonDown;
        }

        private void PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            if (sender is ListBox)
            {
                ListBox dragSource = (ListBox)sender;
                object data = GetDataFromSourceControl(dragSource, e.GetPosition(AssociatedObject));
                if (data != null) DragDrop.DoDragDrop(dragSource, data, DragDropEffects.Move);
            }
        }

        private static object GetDataFromSourceControl(ItemsControl source, Point point)
        {
            UIElement element = source.InputHitTest(point) as UIElement;
            if (element != null)
            {
                object data = DependencyProperty.UnsetValue;
                while (data == DependencyProperty.UnsetValue)
                {
                    data = source.ItemContainerGenerator.ItemFromContainer(element);
                    if (data == DependencyProperty.UnsetValue) element = VisualTreeHelper.GetParent(element) as UIElement;
                    if (element == source) return null;
                }
                if (data != DependencyProperty.UnsetValue) return data;
            }
            return null;
        }
    }

}

TreeViewDragDropBehavior:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interactivity;
using System.Windows.Media;

namespace TreeviewExample
{
    public class TreeViewDragDropBehavior : Behavior<UIElement>
    {
        protected override void OnAttached()
        {
            AssociatedObject.PreviewMouseLeftButtonDown += PreviewMouseLeftButtonDown;
            AssociatedObject.Drop += Tv_Drop;
        }

        private void PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            if (sender is TreeView)
            {
                TreeView dragSource = (TreeView)sender;
                object data = GetDataFromSourceControl(dragSource, e.GetPosition(AssociatedObject));
                if (data != null) DragDrop.DoDragDrop(dragSource, data, DragDropEffects.Move);
            }
        }

        private static object GetDataFromSourceControl(ItemsControl source, Point point)
        {
            UIElement element = source.InputHitTest(point) as UIElement;
            if (element != null)
            {
                object data = DependencyProperty.UnsetValue;
                while (data == DependencyProperty.UnsetValue)
                {
                    data = source.ItemContainerGenerator.ItemFromContainer(element);
                    if (data == DependencyProperty.UnsetValue) element = VisualTreeHelper.GetParent(element) as UIElement;
                    if (element == source) return null;
                }
                if (data != DependencyProperty.UnsetValue) return data;
            }
            return null;
        }

        private void Tv_Drop(object sender, DragEventArgs e)
        {
            if (sender is TreeView)
            {
                TreeView treeView = (TreeView)sender;
                e.Effects = DragDropEffects.None;
                e.Handled = true;
                // Verify that this is a valid drop and then store the drop target
                ViewItem targetItem = (e.OriginalSource as TextBlock)?.DataContext as ViewItem;
                MainViewModel vm = (sender as TreeView).DataContext as MainViewModel;
                if (targetItem != null && vm != null)
                {
                    object data = e.Data.GetData(typeof(ViewItem));
                    ViewItem dragData = (ViewItem)data;
                    targetItem.ViewItems.Add(new ViewItem(dragData.ItemName));
                }
            }
        }
    }
}

Questions:

  1. How to enable my desired feature that move items inside TreeView?
  2. What is the difference of DragDropEffects? I also try to set e.Effects to DragDropEffects.Move, but the result looks the same to me.
  3. What is the difference between MouseLeftButtonDown and PreviewMouseLeftButtonDown. I see lots of similar events that with or without Preview.

Could someone help me with the issue?

What I have tried:

In the PreviewMouseLeftButtonDown function, I add another case for TreeView. But I fail to get the drag data. The return value of e.Data.GetData(typeof(ViewItem)) in the Tv_Drop function is the root in the TreeView.

What I expect:

I want to drag and drop TreeViewItem inside TreeView to reorder the items.

Windows Presentation Foundation
Windows Presentation Foundation
A part of the .NET Framework that provides a unified programming model for building line-of-business desktop applications on Windows.
2,690 questions
C#
C#
An object-oriented and type-safe programming language that has its roots in the C family of languages and includes support for component-oriented programming.
10,420 questions
XAML
XAML
A language based on Extensible Markup Language (XML) that enables developers to specify a hierarchy of objects with a set of properties and logic.
773 questions
{count} votes

Accepted answer
  1. Peter Fleischer (former MVP) 19,306 Reputation points
    2023-09-12T05:19:19.38+00:00

    HI,
    if you want use additional click event (button click) you must change your code.

    MainWindow:

    <Window x:Class="TreeviewExample.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
            xmlns:local="clr-namespace:TreeviewExample"
            mc:Ignorable="d"
            Title="MainWindow" Height="450" Width="800">
      <Window.Resources>
        <local:MainViewModel x:Key="vm"/>
        <local:IntVisConverter x:Key="IntConverter"/>
      </Window.Resources>
      <Grid DataContext="{Binding Source={StaticResource vm}}">
        <Grid.RowDefinitions>
          <RowDefinition Height="*" />
          <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="*" />
          <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
    
        <GroupBox Header="Features" Grid.Column="0" Grid.Row="0" Grid.RowSpan="2" Margin="10,10,10,10">
          <!-- Features ListBox -->
          <ListBox x:Name="featureListBox"
             AllowDrop="True"
             ItemsSource="{Binding ListBoxSource}" Margin="5,10,5,10">
            <i:Interaction.Behaviors>
              <local:ListBoxDragDropBehavior />
            </i:Interaction.Behaviors>
            <ListBox.ItemTemplate>
              <DataTemplate>
                <Grid Margin="0,2">
                  <TextBlock Text="{Binding ItemName}" />
                </Grid>
              </DataTemplate>
            </ListBox.ItemTemplate>
          </ListBox>
        </GroupBox>
    
        <GroupBox Header="Template" Grid.Column="1" Grid.Row="0" Grid.RowSpan="2" Margin="10,10,10,10">
          <Grid>
            <TreeView Name="templateTreeview"
           ItemsSource="{Binding TreeViewSource}"
           FontSize="14"
           AllowDrop="True" Margin="5,5,5,10">
              <i:Interaction.Behaviors>
                <local:TreeViewDragDropBehavior />
              </i:Interaction.Behaviors>
              <TreeView.ItemContainerStyle>
                <Style TargetType="{x:Type TreeViewItem}">
                  <Setter Property="IsExpanded" Value="True" />
                </Style>
              </TreeView.ItemContainerStyle>
              <TreeView.Resources>
                <HierarchicalDataTemplate DataType="{x:Type local:ViewItem}" ItemsSource="{Binding Path=ViewItems}">
                  <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding Path=ItemName}" Margin="0,0,10,0" />
                    <Button Name="DeleteTreeViewItem" 
                            Background="Transparent" 
                            BorderBrush="Transparent"                        
                            Visibility="{Binding Path=IsVisible,Converter={StaticResource IntConverter}}"
                            Command="{Binding Cmd, Source={StaticResource vm}}"
                            CommandParameter="{Binding}">
                      <Image Source="delete_cross.png" Height="15" />
                    </Button>
                  </StackPanel>
                </HierarchicalDataTemplate>
              </TreeView.Resources>
            </TreeView>
          </Grid>
        </GroupBox>
      </Grid>
    </Window>
    

    MainViewModel:

    using System;
    using System.Collections.ObjectModel;
    using System.Windows.Input;
    
    namespace TreeviewExample
    {
    	public class MainViewModel
    	{
    		public ObservableCollection<ViewItem> ListBoxSource { get; set; } = new ObservableCollection<ViewItem>();
    		public ObservableCollection<ViewItem> TreeViewSource { get; set; } = new ObservableCollection<ViewItem>();
    
    		// in ctor load demo data
    		public MainViewModel()
    		{
    			for (int i = 1; i < 20; i++) ListBoxSource.Add(new ViewItem($"ListBoxItem {i}") { Parent = ListBoxSource });
    
    			ViewItem defaultView = new ViewItem("Root", 1) { Parent = TreeViewSource };
    			TreeViewSource.Add(defaultView);
    			for (int i = 1; i < 10; i++) defaultView.ViewItems.Add(new ViewItem($"TreeViewItem {i}") { Parent = defaultView.ViewItems });
    		}
    
    		// property for commands
    		public ICommand Cmd { get => new RelayCommand(CmdExec); }
    
    		private void CmdExec(object obj)
    		{
    			var item = obj as ViewItem;
    			if (item == null) return;
    			item.Parent.Remove(item);
    		}
    	}
    
    #pragma warning disable CS8612 // Nullability of reference types in type doesn't match implicitly implemented member.
    #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
    #pragma warning disable CS8767 // Nullability of reference types in type of parameter doesn't match implicitly implemented member (possibly because of nullability attributes).
    	public class RelayCommand : ICommand
    	{
    		private readonly Predicate<object> _canExecute;
    		private readonly Action<object> _action;
    		public RelayCommand(Action<object> action) { _action = action; _canExecute = null; }
    		public RelayCommand(Action<object> action, Predicate<object> canExecute) { _action = action; _canExecute = canExecute; }
    		public void Execute(object o) => _action(o);
    		public bool CanExecute(object o) => _canExecute == null ? true : _canExecute(o);
    		public event EventHandler CanExecuteChanged
    		{
    			add { CommandManager.RequerySuggested += value; }
    			remove { CommandManager.RequerySuggested -= value; }
    		}
    	}
    
    }
    

    TreeViewDragDropBehavior:

    using System;
    using System.Globalization;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Input;
    using System.Windows.Interactivity;
    using System.Windows.Media;
    
    namespace TreeviewExample
    {
    #pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type.
    #pragma warning disable CS8603 // Possible null reference return.
    #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
    
    	public class TreeViewDragDropBehavior : Behavior<UIElement>
    	{
    		protected override void OnAttached()
    		{
    			AssociatedObject.PreviewMouseLeftButtonDown += tv_PreviewMouseLeftButtonDown;
    			AssociatedObject.MouseMove += tv_MouseMove;
    			AssociatedObject.DragOver += tv_DragOver;
    			AssociatedObject.Drop += Tv_Drop;
    			AssociatedObject.DragLeave += tv_DragLeave;
    		}
    
    		// for saving TreeViewItem to drag
    		TreeViewItem draggedTVI = null;
    
    		// save TreeViewItem to drag
    		private void tv_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    		{
    			draggedTVI = FindAnchestor<TreeViewItem>((DependencyObject)e.OriginalSource);
    		}
    
    		// start Drag&Drop when mouse is moved and there's a saved TreeViewItem
    		private void tv_MouseMove(object sender, MouseEventArgs e)
    		{
    			if (draggedTVI != null)
    			{
    				// Find the data behind the TreeViewItem
    				ViewItem dragData = draggedTVI.DataContext as ViewItem;
    				// Initialize the drag & drop operation
    				DragDrop.DoDragDrop(draggedTVI, dragData, DragDropEffects.Move);
    				// reset saved TreeViewItem
    				draggedTVI = null;
    			}
    		}
    
    		// highlight target
    		private void tv_DragOver(object sender, DragEventArgs e)
    		{
    			TreeViewItem tvi = FindAnchestor<TreeViewItem>((DependencyObject)e.OriginalSource);
    			if (tvi != null) tvi.Background = Brushes.LightBlue;
    		}
    
    		// 
    		private void Tv_Drop(object sender, DragEventArgs e)
    		{
    			// if no data to drop return
    			if (!e.Data.GetDataPresent(typeof(ViewItem))) return;
    			// store the drop target
    			ViewItem targetItem = (e.OriginalSource as TextBlock)?.DataContext as ViewItem;
    			if (targetItem == null) return;
    			ViewItem data = (ViewItem)e.Data.GetData(typeof(ViewItem));
    			targetItem.ViewItems.Add(data);
    			// change parent collection
    			data.Parent.Remove(data);
    			data.Parent = targetItem.ViewItems;
    			// reset background on target TreeViewItem
    			TreeViewItem tvi = FindAnchestor<TreeViewItem>((DependencyObject)e.OriginalSource);
    			if (tvi != null) tvi.Background = Brushes.White;
    		}
    
    		// reset background on leaved possible target TreeViewItem
    		private void tv_DragLeave(object sender, DragEventArgs e)
    		{
    			TreeViewItem tvi = FindAnchestor<TreeViewItem>((DependencyObject)e.OriginalSource);
    			if (tvi != null) tvi.Background = Brushes.White;
    		}
    
    		// Helper to search up the VisualTree
    		private static T FindAnchestor<T>(DependencyObject current) where T : DependencyObject
    		{
    			do
    			{
    				if (current is T) return (T)current;
    				current = VisualTreeHelper.GetParent(current);
    			} while (current != null);
    			return null;
    		}
    	}
    }
    

    x


1 additional answer

Sort by: Most helpful
  1. Peter Fleischer (former MVP) 19,306 Reputation points
    2023-09-11T07:28:58.65+00:00

    Hi,
    you can change your code:

    MainViewModel (include reference to parent collection):

    using System.Collections.ObjectModel;
    
    namespace TreeviewExample
    {
    	public class MainViewModel
    	{
    		public ObservableCollection<ViewItem> ListBoxSource { get; set; } = new ObservableCollection<ViewItem>();
    		public ObservableCollection<ViewItem> TreeViewSource { get; set; } = new ObservableCollection<ViewItem>();
    
    		public MainViewModel()
    		{
    			for (int i = 1; i < 20; i++) ListBoxSource.Add(new ViewItem($"ListBoxItem {i}") { Parent= ListBoxSource });
    
    			ViewItem defaultView = new ViewItem("Root", 1) { Parent = TreeViewSource };
    			TreeViewSource.Add(defaultView);
    			for (int i = 1; i < 10; i++) defaultView.ViewItems.Add(new ViewItem($"TreeViewItem {i}") { Parent= defaultView.ViewItems });
    		}
    	}
    }		
    

    ViewItem (include property for parent collection)

    using System.Collections.ObjectModel;
    
    namespace TreeviewExample
    {
    #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
    	public class ViewItem
    	{
    		public string ItemName { get; set; }
    		public ObservableCollection<ViewItem> ViewItems { get; } = new ObservableCollection<ViewItem>();
    		public int IsVisible { get; set; }
    
    		public ViewItem() { }
    
    		public ViewItem(string name, int IsVisible = 0)
    		{
    			this.ItemName = name;
    			this.IsVisible = IsVisible;
    		}
    
    		public ObservableCollection<ViewItem> Parent { get; set; }
    	}
    }
    

    TreeViewDropBehavior:

    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Input;
    using System.Windows.Interactivity;
    using System.Windows.Media;
    
    namespace TreeviewExample
    {
    #pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type.
    #pragma warning disable CS8602 // Dereference of a possibly null reference.
    #pragma warning disable CS8603 // Possible null reference return.
    
    	public class TreeViewDragDropBehavior : Behavior<UIElement>
    	{
    		protected override void OnAttached()
    		{
    			AssociatedObject.PreviewMouseLeftButtonDown += tv_PreviewMouseLeftButtonDown;
    			AssociatedObject.Drop += Tv_Drop;
    		}
    
    		private void tv_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    		{
    			// Get the dragged TreeViewItem
    			TreeViewItem tvItem = FindAnchestor<TreeViewItem>((DependencyObject)e.OriginalSource);
    
    			// Find the data behind the TreeViewItem
    			ViewItem dragData = tvItem.DataContext as ViewItem;
    
    			// Initialize the drag & drop operation
    			DragDrop.DoDragDrop(tvItem, dragData, DragDropEffects.Move);
    		}
    
    		private void Tv_Drop(object sender, DragEventArgs e)
    		{
    			// check for data
    			if (!e.Data.GetDataPresent(typeof(ViewItem))) return;
    			// store the drop target
    			ViewItem targetItem = (e.OriginalSource as TextBlock)?.DataContext as ViewItem;
                if (targetItem == null) return;
    			ViewItem data = (ViewItem)e.Data.GetData(typeof(ViewItem));
    			targetItem.ViewItems.Add(data);
    			// change parent collection
    			data.Parent.Remove(data);
    			data.Parent= targetItem.ViewItems; 
    		}
    
    		// Helper to search up the VisualTree
    		private static T FindAnchestor<T>(DependencyObject current) where T : DependencyObject
    		{
    			do
    			{
    				if (current is T) return (T)current;
    				current = VisualTreeHelper.GetParent(current);
    			} while (current != null);
    			return null;
    		}
    	}
    }
    
    1 person found this answer helpful.