web-dev-qa-db-fra.com

Personnalisation de TreeView pour permettre la sélection multiple

Le contrôle WPF TreeView intégré ne permet pas la sélection multiple, comme le fait un ListBox. Comment puis-je personnaliser TreeView pour permettre la sélection multiple sans le réécrire.

43
Dylan

Lorsque j'envisage de remplacer le comportement fondamental d'un contrôle, comme une vue arborescente, j'aime toujours considérer la convivialité et l'effort associés à ma décision.

Dans le cas spécifique d'une arborescence, je trouve que le passage à une liste en combinaison avec zéro, un ou plusieurs contrôles permet une solution plus utilisable qui est souvent plus facile à mettre en œuvre.

Par exemple, considérez la boîte de dialogue Ouvrir commune ou l'application Explorateur Windows.

6
Zamboni

J'ai une variation sur l'implémentation SoMoS qui utilise une propriété attachée déclarée sur une dérivation du contrôle TreeView de base pour suivre l'état de sélection des TreeViewItems. Cela conserve le suivi de la sélection sur l'élément TreeViewItem lui-même et hors de l'objet modèle présenté par l'arborescence.

Il s'agit de la nouvelle dérivation de la classe TreeView.

using System.Linq;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Windows.Controls;
using System.Collections;
using System.Collections.Generic;

namespace MultiSelectTreeViewDemo
{
    public sealed class MultiSelectTreeView : TreeView
    {
        #region Fields

        // Used in shift selections
        private TreeViewItem _lastItemSelected;

        #endregion Fields
        #region Dependency Properties

        public static readonly DependencyProperty IsItemSelectedProperty =
            DependencyProperty.RegisterAttached("IsItemSelected", typeof(bool), typeof(MultiSelectTreeView));

        public static void SetIsItemSelected(UIElement element, bool value)
        {
            element.SetValue(IsItemSelectedProperty, value);
        }
        public static bool GetIsItemSelected(UIElement element)
        {
            return (bool)element.GetValue(IsItemSelectedProperty);
        }

        #endregion Dependency Properties
        #region Properties

        private static bool IsCtrlPressed
        {
            get { return Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl); }
        }
        private static bool IsShiftPressed
        {
            get { return Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift); }
        }

        public IList SelectedItems
        {
            get
            {
                var selectedTreeViewItems = GetTreeViewItems(this, true).Where(GetIsItemSelected);
                var selectedModelItems = selectedTreeViewItems.Select(treeViewItem => treeViewItem.Header);

                return selectedModelItems.ToList();
            }
        }

        #endregion Properties
        #region Event Handlers

        protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
        {
            base.OnPreviewMouseDown(e);

            // If clicking on a tree branch expander...
            if (e.OriginalSource is Shape || e.OriginalSource is Grid || e.OriginalSource is Border)
                return;

            var item = GetTreeViewItemClicked((FrameworkElement)e.OriginalSource);
            if (item != null) SelectedItemChangedInternal(item);
        }

        #endregion Event Handlers
        #region Utility Methods

        private void SelectedItemChangedInternal(TreeViewItem tvItem)
        {
            // Clear all previous selected item states if ctrl is NOT being held down
            if (!IsCtrlPressed)
            {
                var items = GetTreeViewItems(this, true);
                foreach (var treeViewItem in items)
                    SetIsItemSelected(treeViewItem, false);
            }

            // Is this an item range selection?
            if (IsShiftPressed && _lastItemSelected != null)
            {
                var items = GetTreeViewItemRange(_lastItemSelected, tvItem);
                if (items.Count > 0)
                {
                    foreach (var treeViewItem in items)
                        SetIsItemSelected(treeViewItem, true);

                    _lastItemSelected = items.Last();
                }
            }
            // Otherwise, individual selection
            else
            {
                SetIsItemSelected(tvItem, true);
                _lastItemSelected = tvItem;
            }
        }
        private static TreeViewItem GetTreeViewItemClicked(DependencyObject sender)
        {
            while (sender != null && !(sender is TreeViewItem))
                sender = VisualTreeHelper.GetParent(sender);
            return sender as TreeViewItem;
        }
        private static List<TreeViewItem> GetTreeViewItems(ItemsControl parentItem, bool includeCollapsedItems, List<TreeViewItem> itemList = null)
        {
            if (itemList == null)
                itemList = new List<TreeViewItem>();

            for (var index = 0; index < parentItem.Items.Count; index++)
            {
                var tvItem = parentItem.ItemContainerGenerator.ContainerFromIndex(index) as TreeViewItem;
                if (tvItem == null) continue;

                itemList.Add(tvItem);
                if (includeCollapsedItems || tvItem.IsExpanded)
                    GetTreeViewItems(tvItem, includeCollapsedItems, itemList);
            }
            return itemList;
        }
        private List<TreeViewItem> GetTreeViewItemRange(TreeViewItem start, TreeViewItem end)
        {
            var items = GetTreeViewItems(this, false);

            var startIndex = items.IndexOf(start);
            var endIndex = items.IndexOf(end);
            var rangeStart = startIndex > endIndex || startIndex == -1 ? endIndex : startIndex;
            var rangeCount = startIndex > endIndex ? startIndex - endIndex + 1 : endIndex - startIndex + 1;

            if (startIndex == -1 && endIndex == -1)
                rangeCount = 0;

            else if (startIndex == -1 || endIndex == -1)
                rangeCount = 1;

            return rangeCount > 0 ? items.GetRange(rangeStart, rangeCount) : new List<TreeViewItem>();
        }

        #endregion Utility Methods
    }
}

Et voici le XAML. Notez que la partie saillante est le remplacement des deux déclencheurs qui utilisent la propriété singulière 'IsSelected' par la nouvelle propriété attachée 'IsItemSelected' dans MultiSelectTreeViewItemStyle pour atteindre l'état visuel.

Notez également que je n'agrège pas le nouveau contrôle TreeView dans un UserControl.

<Window 
    x:Class="MultiSelectTreeViewDemo.MainWindow"
    xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:MultiSelectTreeViewDemo"
    Title="MultiSelect TreeView Demo" Height="350" Width="525">

    <Window.Resources>
        <local:DemoViewModel x:Key="ViewModel"/>
        <Style x:Key="TreeViewItemFocusVisual">
            <Setter Property="Control.Template">
                <Setter.Value>
                    <ControlTemplate>
                        <Rectangle/>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <SolidColorBrush x:Key="TreeViewItem.TreeArrow.Static.Checked.Fill" Color="#FF595959"/>
        <SolidColorBrush x:Key="TreeViewItem.TreeArrow.Static.Checked.Stroke" Color="#FF262626"/>
        <SolidColorBrush x:Key="TreeViewItem.TreeArrow.MouseOver.Stroke" Color="#FF1BBBFA"/>
        <SolidColorBrush x:Key="TreeViewItem.TreeArrow.MouseOver.Fill" Color="Transparent"/>
        <SolidColorBrush x:Key="TreeViewItem.TreeArrow.MouseOver.Checked.Stroke" Color="#FF262626"/>
        <SolidColorBrush x:Key="TreeViewItem.TreeArrow.MouseOver.Checked.Fill" Color="#FF595959"/>
        <PathGeometry x:Key="TreeArrow" Figures="M0,0 L0,6 L6,0 z"/>
        <SolidColorBrush x:Key="TreeViewItem.TreeArrow.Static.Fill" Color="Transparent"/>
        <SolidColorBrush x:Key="TreeViewItem.TreeArrow.Static.Stroke" Color="#FF989898"/>
        <Style x:Key="ExpandCollapseToggleStyle" TargetType="{x:Type ToggleButton}">
            <Setter Property="Focusable" Value="False"/>
            <Setter Property="Width" Value="16"/>
            <Setter Property="Height" Value="16"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ToggleButton}">
                        <Border Background="Transparent" Height="16" Padding="5,5,5,5" Width="16">
                            <Path x:Name="ExpandPath" Data="{StaticResource TreeArrow}" Fill="{StaticResource TreeViewItem.TreeArrow.Static.Fill}" Stroke="{StaticResource TreeViewItem.TreeArrow.Static.Stroke}">
                                <Path.RenderTransform>
                                    <RotateTransform Angle="135" CenterY="3" CenterX="3"/>
                                </Path.RenderTransform>
                            </Path>
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsChecked" Value="True">
                                <Setter Property="RenderTransform" TargetName="ExpandPath">
                                    <Setter.Value>
                                        <RotateTransform Angle="180" CenterY="3" CenterX="3"/>
                                    </Setter.Value>
                                </Setter>
                                <Setter Property="Fill" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.Static.Checked.Fill}"/>
                                <Setter Property="Stroke" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.Static.Checked.Stroke}"/>
                            </Trigger>
                            <Trigger Property="IsMouseOver" Value="True">
                                <Setter Property="Stroke" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.MouseOver.Stroke}"/>
                                <Setter Property="Fill" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.MouseOver.Fill}"/>
                            </Trigger>
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <Condition Property="IsMouseOver" Value="True"/>
                                    <Condition Property="IsChecked" Value="True"/>
                                </MultiTrigger.Conditions>
                                <Setter Property="Stroke" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.MouseOver.Checked.Stroke}"/>
                                <Setter Property="Fill" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.MouseOver.Checked.Fill}"/>
                            </MultiTrigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <Style x:Key="MultiSelectTreeViewItemStyle" TargetType="{x:Type TreeViewItem}">
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
            <Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
            <Setter Property="Padding" Value="1,0,0,0"/>
            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
            <Setter Property="FocusVisualStyle" Value="{StaticResource TreeViewItemFocusVisual}"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type TreeViewItem}">
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition MinWidth="19" Width="Auto"/>
                                <ColumnDefinition Width="Auto"/>
                                <ColumnDefinition Width="*"/>
                            </Grid.ColumnDefinitions>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="Auto"/>
                                <RowDefinition/>
                            </Grid.RowDefinitions>
                            <ToggleButton 
                                x:Name="Expander" 
                                ClickMode="Press" 
                                IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}}" 
                                Style="{StaticResource ExpandCollapseToggleStyle}"/>
                            <Border 
                                x:Name="Bd" 
                                BorderBrush="{TemplateBinding BorderBrush}" 
                                BorderThickness="{TemplateBinding BorderThickness}" 
                                Background="{TemplateBinding Background}" 
                                Grid.Column="1" 
                                Padding="{TemplateBinding Padding}" 
                                SnapsToDevicePixels="true">
                                <ContentPresenter 
                                    x:Name="PART_Header" 
                                    ContentSource="Header" 
                                    HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" 
                                    SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                            </Border>
                            <ItemsPresenter 
                                x:Name="ItemsHost" 
                                Grid.ColumnSpan="2" 
                                Grid.Column="1" 
                                Grid.Row="1"/>
                        </Grid>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsExpanded" Value="false">
                                <Setter Property="Visibility" TargetName="ItemsHost" Value="Collapsed"/>
                            </Trigger>
                            <Trigger Property="HasItems" Value="false">
                                <Setter Property="Visibility" TargetName="Expander" Value="Hidden"/>
                            </Trigger>
                            <!--Trigger Property="IsSelected" Value="true"-->
                            <Trigger Property="local:MultiSelectTreeView.IsItemSelected" Value="true">
                                <Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
                                <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}"/>
                            </Trigger>
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <!--Condition Property="IsSelected" Value="true"/-->
                                    <Condition Property="local:MultiSelectTreeView.IsItemSelected" Value="true"/>
                                    <Condition Property="IsSelectionActive" Value="false"/>
                                </MultiTrigger.Conditions>
                                <Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightBrushKey}}"/>
                                <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}}"/>
                            </MultiTrigger>
                            <Trigger Property="IsEnabled" Value="false">
                                <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Style.Triggers>
                <Trigger Property="VirtualizingPanel.IsVirtualizing" Value="true">
                    <Setter Property="ItemsPanel">
                        <Setter.Value>
                            <ItemsPanelTemplate>
                                <VirtualizingStackPanel/>
                            </ItemsPanelTemplate>
                        </Setter.Value>
                    </Setter>
                </Trigger>
            </Style.Triggers>
        </Style>
    </Window.Resources>

    <Grid
        Background="WhiteSmoke"
        DataContext="{DynamicResource ViewModel}">
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <local:MultiSelectTreeView
            x:Name="multiSelectTreeView"
            ItemContainerStyle="{StaticResource MultiSelectTreeViewItemStyle}"
            ItemsSource="{Binding FoodGroups}">
            <local:MultiSelectTreeView.ItemTemplate>
                <HierarchicalDataTemplate ItemsSource="{Binding Children}">
                    <Grid>
                        <TextBlock FontSize="14" Text="{Binding Name}"/>
                    </Grid>
                </HierarchicalDataTemplate>
            </local:MultiSelectTreeView.ItemTemplate>
        </local:MultiSelectTreeView>
        <Button 
            Grid.Row="1" 
            Margin="0,10" 
            Padding="20,2"
            HorizontalAlignment="Center"
            Content="Get Selections" 
            Click="GetSelectionsButton_OnClick"/>
    </Grid>
</Window>

Et voici un modèle de vue ringard pour le conduire (à des fins de démonstration).

using System.Collections.ObjectModel;

namespace MultiSelectTreeViewDemo
{
    public sealed class DemoViewModel
    {
        public ObservableCollection<FoodItem> FoodGroups { get; set; }

        public DemoViewModel()
        {
            var redMeat = new FoodItem { Name = "Reds" };
            redMeat.Add(new FoodItem { Name = "Beef" });
            redMeat.Add(new FoodItem { Name = "Buffalo" });
            redMeat.Add(new FoodItem { Name = "Lamb" });

            var whiteMeat = new FoodItem { Name = "Whites" };
            whiteMeat.Add(new FoodItem { Name = "Chicken" });
            whiteMeat.Add(new FoodItem { Name = "Duck" });
            whiteMeat.Add(new FoodItem { Name = "Pork" });
            var meats = new FoodItem { Name = "Meats", Children = { redMeat, whiteMeat } };

            var veggies = new FoodItem { Name = "Vegetables" };
            veggies.Add(new FoodItem { Name = "Potato" });
            veggies.Add(new FoodItem { Name = "Corn" });
            veggies.Add(new FoodItem { Name = "Spinach" });

            var fruits = new FoodItem { Name = "Fruits" };
            fruits.Add(new FoodItem { Name = "Apple" });
            fruits.Add(new FoodItem { Name = "Orange" });
            fruits.Add(new FoodItem { Name = "Pear" });

            FoodGroups = new ObservableCollection<FoodItem> { meats, veggies, fruits };
        }
    }
    public sealed class FoodItem
    {
        public string Name { get; set; }
        public ObservableCollection<FoodItem> Children { get; set; }

        public FoodItem()
        {
            Children = new ObservableCollection<FoodItem>();
        }
        public void Add(FoodItem item)
        {
            Children.Add(item);
        }
    }
}

Et voici le bouton click-handler sur le code derrière MainWindow qui montre les sélections dans un MessageBox.

    private void GetSelectionsButton_OnClick(object sender, RoutedEventArgs e)
    {
        var selectedMesg = "";
        var selectedItems = multiSelectTreeView.SelectedItems;

        if (selectedItems.Count > 0)
        {
            selectedMesg = selectedItems.Cast<FoodItem>()
                .Where(modelItem => modelItem != null)
                .Aggregate(selectedMesg, (current, modelItem) => current + modelItem.Name + Environment.NewLine);
        }
        else
            selectedMesg = "No selected items!";

        MessageBox.Show(selectedMesg, "MultiSelect TreeView Demo", MessageBoxButton.OK);
    }

J'espère que cela t'aides.

6
Nishan Hossepian

J'ai simplifié cette tâche en ajoutant une case à cocher avant le texte pour chaque élément treeviewitem.

J'ai donc créé un panneau de dock avec 2 éléments à l'intérieur: case à cocher + bloc de texte.

Alors...

XAML

<TreeView x:Name="treeViewProcesso" Margin="1,30.351,1,5"  BorderBrush="{x:Null}" MinHeight="250" VerticalContentAlignment="Top" BorderThickness="0" >
  <TreeViewItem Header="Documents" x:Name="treeView" IsExpanded="True" DisplayMemberPath="DocumentsId" >            
  </TreeViewItem>
</TreeView>

CS

TreeViewItem treeViewItem = new TreeViewItem();
DockPanel dp = new DockPanel();
CheckBox cb = new CheckBox();
TextBlock tb = new TextBlock();
tb.Text = "Item";
dp.Children.Add(cb);
dp.Children.Add(tb);
treeViewItem.Header = dp;
treeViewItem.Selected += new RoutedEventHandler(item_Selected);
treeView.Items.Add(treeViewItem);

Et puis vous pouvez accéder à la valeur de la case à cocher:

void item_Selected(object sender, RoutedEventArgs e)
{
  selectedTVI = ((TreeViewItem)sender);

  CheckBox cb = (Checkbox)((DockPanel)selectedTVI.Header).Children[0];
}

C'est une façon simple de faire si vous n'avez besoin de rien de complexe.

3
Web Developer

J'ai finalement fini de coder mon propre CustomControl contenant un TreeView à l'intérieur. Sur la base du travail des autres, la clé de la fonctionnalité réside dans le fait que tous les éléments du modèle de TreeView héritent de l'interface ISelectable:

public interface ISelectable
{
    public bool IsSelected {get; set}
}

De cette façon, nous aurons une nouvelle propriété 'IsSelected' qui n'a rien à voir avec le TreeViewItem IsSelected. Nous avons juste besoin de styliser notre arbre pour qu'il gère la propriété IsSelected du modèle. Voici le code (il utilise les bibliothèques Drag & drop disponibles sur http://code.google.com/p/gong-wpf-dragdrop/ ):

XAML

<UserControl x:Class="Picis.Wpf.Framework.ExtendedControls.TreeViewEx.TreeViewEx"

         xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
         xmlns:DragAndDrop="clr-namespace:Picis.Wpf.Framework.DragAndDrop">

<TreeView ItemsSource="{Binding ItemsSource, RelativeSource={RelativeSource AncestorType=UserControl}}" 
          ItemTemplate="{Binding ItemTemplate, RelativeSource={RelativeSource AncestorType=UserControl}}" 
          ItemContainerStyle="{Binding ItemContainerStyle, RelativeSource={RelativeSource AncestorType=UserControl}}" 
          DragAndDrop:DragDrop.DropHandler ="{Binding DropHandler, RelativeSource={RelativeSource AncestorType=UserControl}}" 
          PreviewMouseDown="TreeViewOnPreviewMouseDown" 
          PreviewMouseUp="TreeViewOnPreviewMouseUp"
          x:FieldModifier="private" x:Name="InnerTreeView" >
    <TreeView.Resources>
        <Style TargetType="TreeViewItem">
            <Style.Resources>
                <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="White" />
                <SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="Black" />
                <SolidColorBrush x:Key="{x:Static SystemColors.ControlBrushKey}" Color="White" />
            </Style.Resources>
        </Style>
    </TreeView.Resources>
</TreeView>

C #:

using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using GongSolutions.Wpf.DragDrop;
using DragDrop = GongSolutions.Wpf.DragDrop;

namespace <yournamespace>.TreeViewEx
{
public partial class TreeViewEx : UserControl
{
    #region Attributes

    private TreeViewItem _lastItemSelected; // Used in shift selections
    private TreeViewItem _itemToCheck; // Used when clicking on a selected item to check if we want to deselect it or to drag the current selection
    private bool _isDragEnabled;
    private bool _isDropEnabled;

    #endregion

    #region Dependency Properties

    public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(IEnumerable<ISelectable>), typeof(TreeViewEx));

    public IEnumerable<ISelectable> ItemsSource
    {
        get
        {
            return (IEnumerable<ISelectable>)this.GetValue(TreeViewEx.ItemsSourceProperty);
        }
        set
        {
            this.SetValue(TreeViewEx.ItemsSourceProperty, value);
        }
    }

    public static readonly DependencyProperty ItemTemplateProperty = DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(TreeViewEx));

    public DataTemplate ItemTemplate
    {
        get
        {
            return (DataTemplate)GetValue(TreeViewEx.ItemTemplateProperty);
        }
        set
        {
            SetValue(TreeViewEx.ItemTemplateProperty, value);
        }
    }

    public static readonly DependencyProperty ItemContainerStyleProperty = DependencyProperty.Register("ItemContainerStyle", typeof(Style), typeof(TreeViewEx));

    public Style ItemContainerStyle
    {
        get
        {
            return (Style)GetValue(TreeViewEx.ItemContainerStyleProperty);
        }
        set
        {
            SetValue(TreeViewEx.ItemContainerStyleProperty, value);
        }
    }

    public static readonly DependencyProperty DropHandlerProperty = DependencyProperty.Register("DropHandler", typeof(IDropTarget), typeof(TreeViewEx));

    public IDropTarget DropHandler
    {
        get
        {
            return (IDropTarget)GetValue(TreeViewEx.DropHandlerProperty);
        }
        set
        {
            SetValue(TreeViewEx.DropHandlerProperty, value);
        }
    }

    #endregion

    #region Properties

    public bool IsDragEnabled
    {
        get
        {
            return _isDragEnabled;
        }
        set
        {
            if (_isDragEnabled != value)
            {
                _isDragEnabled = value;
                DragDrop.SetIsDragSource(this.InnerTreeView, _isDragEnabled);
            }
        }
    }

    public bool IsDropEnabled
    {
        get
        {
            return _isDropEnabled;
        }
        set
        {
            if (_isDropEnabled != value)
            {
                _isDropEnabled = value;
                DragDrop.SetIsDropTarget(this.InnerTreeView, _isDropEnabled);
            }
        }
    }

    #endregion

    #region Public Methods

    public TreeViewEx()
    {
        InitializeComponent();
    }

    #endregion

    #region Event Handlers

    private void TreeViewOnPreviewMouseDown(object sender, MouseButtonEventArgs e)
    {
        if (e.OriginalSource is Shape || e.OriginalSource is Grid || e.OriginalSource is Border) // If clicking on the + of the tree
            return;

        TreeViewItem item = this.GetTreeViewItemClicked((FrameworkElement)e.OriginalSource);

        if (item != null && item.Header != null)
        {
            this.SelectedItemChangedHandler(item);
        }
    }

    // Check done to avoid deselecting everything when clicking to drag
    private void TreeViewOnPreviewMouseUp(object sender, MouseButtonEventArgs e)
    {
        if (_itemToCheck != null)
        {
            TreeViewItem item = this.GetTreeViewItemClicked((FrameworkElement)e.OriginalSource);

            if (item != null && item.Header != null)
            {
                if (!TreeViewEx.IsCtrlPressed)
                {
                    GetTreeViewItems(true).Select(t => t.Header).Cast<ISelectable>().ToList().ForEach(f => f.IsSelected = false);
                    ((ISelectable)_itemToCheck.Header).IsSelected = true;
                    _lastItemSelected = _itemToCheck;
                }
                else
                {
                    ((ISelectable)_itemToCheck.Header).IsSelected = false;
                    _lastItemSelected = null;
                }
            }
        }
    }

    #endregion

    #region Private Methods

    private void SelectedItemChangedHandler(TreeViewItem item)
    {
        ISelectable content = (ISelectable)item.Header;

        _itemToCheck = null;

        if (content.IsSelected)
        {
            // Check it at the mouse up event to avoid deselecting everything when clicking to drag
            _itemToCheck = item;
        }
        else
        {
            if (!TreeViewEx.IsCtrlPressed)
            {
                GetTreeViewItems(true).Select(t => t.Header).Cast<ISelectable>().ToList().ForEach(f => f.IsSelected = false);
            }

            if (TreeViewEx.IsShiftPressed && _lastItemSelected != null)
            {
                foreach (TreeViewItem tempItem in GetTreeViewItemsBetween(_lastItemSelected, item))
                {
                    ((ISelectable)tempItem.Header).IsSelected = true;
                    _lastItemSelected = tempItem;
                }
            }
            else
            {
                content.IsSelected = true;
                _lastItemSelected = item;
            }
        }
    }

    private static bool IsCtrlPressed
    {
        get
        {
            return Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl);
        }
    }

    private static bool IsShiftPressed
    {
        get
        {
            return Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift);
        }
    }

    private TreeViewItem GetTreeViewItemClicked(UIElement sender)
    {
        Point point = sender.TranslatePoint(new Point(0, 0), this.InnerTreeView);
        DependencyObject visualItem = this.InnerTreeView.InputHitTest(point) as DependencyObject;
        while (visualItem != null && !(visualItem is TreeViewItem))
        {
            visualItem = VisualTreeHelper.GetParent(visualItem);
        }

        return visualItem as TreeViewItem;
    }

    private IEnumerable<TreeViewItem> GetTreeViewItemsBetween(TreeViewItem start, TreeViewItem end)
    {
        List<TreeViewItem> items = this.GetTreeViewItems(false);

        int startIndex = items.IndexOf(start);
        int endIndex = items.IndexOf(end);

        // It's possible that the start element has been removed after it was selected,
        // I don't find a way to happen on the end but I add the code to handle the situation just in case
        if (startIndex == -1 && endIndex == -1)
        {
            return new List<TreeViewItem>();
        }
        else if (startIndex == -1)
        {
            return new List<TreeViewItem>() {end};
        }
        else if (endIndex == -1)
        {
            return new List<TreeViewItem>() { start };
        }
        else
        {
            return startIndex > endIndex ? items.GetRange(endIndex, startIndex - endIndex + 1) : items.GetRange(startIndex, endIndex - startIndex + 1); 
        }
    }

    private List<TreeViewItem> GetTreeViewItems(bool includeCollapsedItems)
    {
        List<TreeViewItem> returnItems = new List<TreeViewItem>();

        for (int index = 0; index < this.InnerTreeView.Items.Count; index++)
        {
            TreeViewItem item = (TreeViewItem)this.InnerTreeView.ItemContainerGenerator.ContainerFromIndex(index);
            returnItems.Add(item);
            if (includeCollapsedItems || item.IsExpanded)
            {
                returnItems.AddRange(GetTreeViewItemItems(item, includeCollapsedItems));                    
            }
        }

        return returnItems;
    }

    private static IEnumerable<TreeViewItem> GetTreeViewItemItems(TreeViewItem treeViewItem, bool includeCollapsedItems)
    {
        List<TreeViewItem> returnItems = new List<TreeViewItem>();

        for (int index = 0; index < treeViewItem.Items.Count; index++)
        {
            TreeViewItem item = (TreeViewItem)treeViewItem.ItemContainerGenerator.ContainerFromIndex(index);
            if (item != null)
            {
                returnItems.Add(item);
                if (includeCollapsedItems || item.IsExpanded)
                {
                    returnItems.AddRange(GetTreeViewItemItems(item, includeCollapsedItems));
                }
            }
        }

        return returnItems;
    }

    #endregion
}
}
1