Showing posts with label Attached Property. Show all posts
Showing posts with label Attached Property. Show all posts

Thursday, August 21, 2014

WPF TreeView with Multiple Selection

With the WPF TreeView it is not possible to select multiple items. But it can be easily extended. Therefore an Attached Property can be used. First we are adding the used namespace to the control.
xmlns:e="clr-namespace:MultipleSelectionTreeView;
                                            assembly=MultipleSelectionTreeView"
Then we can add the Attached Property to the TreeView. With IsMultipleSelection="True" we activate the multiple selection in the TreeView. With the property SelectedItems we can bind the multiple selected items to a property of the DataContext. We can define the style of a multiple selected TreeViewItem within a Style Trigger. Therefore the property IsItemSelected is used.
<TreeView ItemsSource="{Binding Elements}"
          MinHeight="20"
          e:TreeViewMultipleSelectionAttached.IsMultipleSelection="True"
          e:TreeViewMultipleSelectionAttached.SelectedItems=
                                                   "{Binding SelectedElements}">
    <TreeView.Resources>
        <Style x:Key="{x:Type TreeViewItem}" BasedOn="{StaticResource 
                                                        {x:Type TreeViewItem}}" 
                                                        TargetType=
                                                        "{x:Type TreeViewItem}">
            <Style.Triggers>
                <Trigger 
                   Property="e:TreeViewMultipleSelectionAttached.IsItemSelected"
                         Value="True">
                    <Setter Property="Background"
                            Value="LightGreen" />
                </Trigger>
            </Style.Triggers>
        </Style>
        <HierarchicalDataTemplate DataType="{x:Type e:Element}"
                                  ItemsSource="{Binding ChildElements}">
            <StackPanel Orientation="Horizontal"
                        VerticalAlignment="Stretch"
                        Margin="0,2,0,2">
                <TextBlock Text="{Binding ElementName}"
                           Margin="5,0,0,0"
                           VerticalAlignment="Center" />
            </StackPanel>
        </HierarchicalDataTemplate>
        <HierarchicalDataTemplate DataType="{x:Type e:ChildElement}">
            <StackPanel Orientation="Horizontal"
                        VerticalAlignment="Stretch"
                        Margin="0,2,0,2">
                <TextBlock Text="{Binding ElementName}"
                           VerticalAlignment="Center"
                           Margin="5,0,0,0" />
            </StackPanel>
        </HierarchicalDataTemplate>
    </TreeView.Resources>
</TreeView>
Now have a look at the implementation of the Attached Property. To create a custom Attached Property a static DependencyProperty is defined by using the RegisterAttached method. This method has the possibility to set a PropertyMetadata as parameter in that a PropertyChangedCallback can be defined by using Delegates. In the callback method an event handler is added or removed to DependencyObject. First we define the IsMultipleSelection property. With that a TreeView is marked in XAML to have multiple selection abilities.
    public static readonly DependencyProperty IsMultipleSelectionProperty =
        DependencyProperty.RegisterAttached(
            "IsMultipleSelection",
            typeof(Boolean),
            typeof(TreeViewMultipleSelectionAttached),
            new PropertyMetadata(false, OnMultipleSelectionPropertyChanged));
 
    public static bool GetIsMultipleSelection(TreeView element)
    {
        return (bool)element.GetValue(IsMultipleSelectionProperty);
    }
 
    public static void SetIsMultipleSelection(TreeView element, Boolean value)
    {
        element.SetValue(IsMultipleSelectionProperty, value);
    }
In the RegisterAttached method we have defined a PropertyMetadata object that defines a call back method (OnMultipleSelectionPropertyChanged) that is called if the property has changed. In that method a handler for the MouseLeftButtonDownEvent of TreeViewItem is added or removed to the TreeView. Remember to mark the handledEventsToo parameter in the AddHandler method as true to allow calling the handler OnTreeViewItemClicked correctly.
    private static void OnMultipleSelectionPropertyChanged(DependencyObject d,
                                         DependencyPropertyChangedEventArgs e)
    {
        TreeView treeView = d as TreeView;
 
        if (treeView != null)
        {
            if (e.NewValue is bool)
            {
                if ((bool)e.NewValue)
                {
                    treeView.AddHandler(TreeViewItem.MouseLeftButtonDownEvent,
                      new MouseButtonEventHandler(OnTreeViewItemClicked), true);
                }
                else
                {
                   treeView.RemoveHandler(TreeViewItem.MouseLeftButtonDownEvent,
                        new MouseButtonEventHandler(OnTreeViewItemClicked));
                }
            }
        }
    }
The OnTreeViewItemClicked method is now called on each click at a TreeViewItem if IsMultipleSelection for a TreeView is set to true.
    private static void OnTreeViewItemClicked(object sender, MouseButtonEventArgs e)
    {
        TreeViewItem treeViewItem = FindTreeViewItem(
                                        e.OriginalSource as DependencyObject);
        TreeView treeView = sender as TreeView;
 
        if (treeViewItem != null && treeView != null)
        {
            if (Keyboard.Modifiers == ModifierKeys.Control)
            {
                SelectMultipleItemsRandomly(treeView, treeViewItem);
            }
            else if (Keyboard.Modifiers == ModifierKeys.Shift)
            {
                SelectMultipleItemsContinuously(treeView, treeViewItem);
            }
            else
            {
                SelectSingleItem(treeView, treeViewItem);
            }
        }
    }
First we need to find the TreeViewItem that is invoked with the click. This happens with the recursive FindTreeViewItem method.
    private static TreeViewItem FindTreeViewItem(DependencyObject dependencyObject)
    {
        if (dependencyObject == null)
        {
            return null;
        }
 
        TreeViewItem treeViewItem = dependencyObject as TreeViewItem;
        if (treeViewItem != null)
        {
            return treeViewItem;
        }
 
        return FindTreeViewItem(VisualTreeHelper.GetParent(dependencyObject));
    }
Then the OnTreeViewItemClicked method checks what kind of click is happening. There are Left-Mouse-Button (LMB) + Ctrl, LMB + Shift, and all other cases with LMB. In the third case a single click is performed.
    private static void SelectSingleItem(TreeView treeView,
                                                    TreeViewItem treeViewItem)
    {
        // first deselect all items
        DeSelectAllItems(treeView, null);
        SetIsItemSelected(treeViewItem, true);
        SetStartItem(treeView, treeViewItem);
    }
Therefore at first all items are deselected by calling the recursive method DeSelectAllItems. 
    private static void DeSelectAllItems(TreeView treeView,
                                                 TreeViewItem treeViewItem)
    {
        if (treeView != null)
        {
            for (int i = 0; i < treeView.Items.Count; i++)
            {
                TreeViewItem item = treeView.ItemContainerGenerator.
                                           ContainerFromIndex(i) as TreeViewItem;
                if (item != null)
                {
                    SetIsItemSelected(item, false);
                    DeSelectAllItems(null, item);
                }
            } 
        }
        else
        {
            for (int i = 0; i < treeViewItem.Items.Count; i++)
            {
                TreeViewItem item = treeViewItem.ItemContainerGenerator.
                                           ContainerFromIndex(i) as TreeViewItem;
                if (item != null)
                {
                    SetIsItemSelected(item, false);
                    DeSelectAllItems(null, item);
                }
            } 
        }
    }
Selection and deselection is happening by the DependencyProperty IsItemSelected that we have also defined. This property is used in XAML to apply a certain style on the selected TreeViewItems.
    public static readonly DependencyProperty IsItemSelectedProperty =
        DependencyProperty.RegisterAttached(
            "IsItemSelected",
            typeof(Boolean),
            typeof(TreeViewMultipleSelectionAttached),
            new PropertyMetadata(false, OnIsItemSelectedPropertyChanged));
 
    public static bool GetIsItemSelected(TreeViewItem element)
    {
        return (bool)element.GetValue(IsItemSelectedProperty);
    }
 
    public static void SetIsItemSelected(TreeViewItem element, Boolean value)
    {
        element.SetValue(IsItemSelectedProperty, value);
    }
In the RegisterAttached method we have defined a PropertyMetadata object that defines a call back method (OnIsItemSelectedPropertyChanged) that is called if the property (selection of the TreeViewItem) has changed. In that method the Header of TreeViewItem will be added to or removed to a List of all selected TreeViewItems. This List is associated with the corresponding TreeView.
    private static void OnIsItemSelectedPropertyChanged(DependencyObject d,
                                           DependencyPropertyChangedEventArgs e)
    {
        TreeViewItem treeViewItem = d as TreeViewItem;
        TreeView treeView = FindTreeView(treeViewItem);
        if (treeViewItem != null && treeView != null)
        {
            var selectedItems = GetSelectedItems(treeView);
            if (selectedItems != null)
            {
                if (GetIsItemSelected(treeViewItem))
                {
                    selectedItems.Add(treeViewItem.Header);
                }
                else
                {
                    selectedItems.Remove(treeViewItem.Header);
                }
            }
        }
    }
To find the TreeView corresponding to the TreeViewItem the recursive method FindTreeView is used.
    private static TreeView FindTreeView(DependencyObject dependencyObject)
    {
        if (dependencyObject == null)
        {
            return null;
        }
 
        TreeView treeView = dependencyObject as TreeView;
        if (treeView != null)
        {
            return treeView;
        }
 
        return FindTreeView(VisualTreeHelper.GetParent(dependencyObject));
    }
To associate the List of all selected TreeViewItems with the corresponding TreeView the DependencyProperty SelectedItems is used. This property can be used in XAML to bind the multiple selected items to a property of the DataContext.
    public static readonly DependencyProperty SelectedItemsProperty =
        DependencyProperty.RegisterAttached(
            "SelectedItems",
            typeof(IList),
            typeof(TreeViewMultipleSelectionAttached),
            new PropertyMetadata());
 
    public static IList GetSelectedItems(TreeView element)
    {
        return (IList)element.GetValue(SelectedItemsProperty);
    }
 
    public static void SetSelectedItems(TreeView element, IList value)
    {
        element.SetValue(SelectedItemsProperty, value);
    }
Now let's go back to the SelectSingleItem method. After deselecting all TreeViewItems by setting the IsItemSelected DependencyProperty to false, the clicked TreeViewItem is marked as selected by setting the IsItemSelected DependencyProperty to true. After that the TreeViewItem is marked as StartItem within the corresponding TreeView. Therefore the private StartItem DependencyProperty is used. The StartItem DependencyProperty is used as starting point for a subsequent multiple selection where a continuous range is selected.
    private static readonly DependencyProperty StartItemProperty =
        DependencyProperty.RegisterAttached(
            "StartItem",
            typeof(TreeViewItem),
            typeof(TreeViewMultipleSelectionAttached),
            new PropertyMetadata());
 
    private static TreeViewItem GetStartItem(TreeView element)
    {
        return (TreeViewItem)element.GetValue(StartItemProperty);
    }
 
    private static void SetStartItem(TreeView element, TreeViewItem value)
    {
        element.SetValue(StartItemProperty, value);
    }
If a LMB + Ctrl is performed the method SelectMultipleItemsRandomly is called. In this method the IsItemSelected DependencyProperty of the clicked TreeViewItem is toggled. Furthermore the StartItem DependencyProperty will be set of not already set or unset if no TreeViewItem is selected anymore.
    private static void SelectMultipleItemsRandomly(TreeView treeView,
                                                    TreeViewItem treeViewItem)
    {
        SetIsItemSelected(treeViewItem, !GetIsItemSelected(treeViewItem));
        if (GetStartItem(treeView) == null)
        {
            if (GetIsItemSelected(treeViewItem))
            {
                SetStartItem(treeView, treeViewItem);
            }
        }
        else
        {
            if (GetSelectedItems(treeView).Count == 0)
            {
                SetStartItem(treeView, null);
            }
        }
    }
If a LMB + Shift is performed the method SelectMutlipleItemsContinuously is called. If no StartItem is set, no action takes place. If the selected TreeViewItem is equal to the startItem then the SelectSingleItem method is called and then the method is returned. If both conditions not met multiple continuously TreeViewItems selection takes place.
    private static void SelectMultipleItemsContinuously(TreeView treeView,
                                                     TreeViewItem treeViewItem)
    {
        TreeViewItem startItem = GetStartItem(treeView);
        if (startItem != null)
        {
            if (startItem == treeViewItem)
            {
                SelectSingleItem(treeView, treeViewItem);
                return;
            }
 
            ICollection<TreeViewItem> allItems = new List<TreeViewItem>();
            GetAllItems(treeView, null, allItems);
            DeSelectAllItems(treeView, null);
            bool isBetween = false;
            foreach (var item in allItems)
            {
                if (item == treeViewItem || item == startItem)
                {
                    // toggle to true if first element is found and
                    // back to false if last element is found
                    isBetween = !isBetween;
 
                    // set boundary element
                    SetIsItemSelected(item, true);
                    continue;
                }
 
                if (isBetween)
                {
                    SetIsItemSelected(item, true);
                }
            }
        }
    }
With the GetAllItems method all TreeViewItems of a TreeView are recursively collected into a collection. Furthermore all items are deselected by the DeSelectAllItems method.
    private static void GetAllItems(TreeView treeView, TreeViewItem treeViewItem,
                                    ICollection<TreeViewItem> allItems)
    {
        if (treeView != null)
        {
            for (int i = 0; i < treeView.Items.Count; i++)
            {
                TreeViewItem item = treeView.ItemContainerGenerator.
                                           ContainerFromIndex(i) as TreeViewItem;
                if (item != null)
                {
                    allItems.Add(item);
                    GetAllItems(null, item, allItems);
                }
            }
        }
        else
        {
            for (int i = 0; i < treeViewItem.Items.Count; i++)
            {
                TreeViewItem item = treeViewItem.ItemContainerGenerator.
                                           ContainerFromIndex(i) as TreeViewItem;
                if (item != null)
                {
                    allItems.Add(item);
                    GetAllItems(null, item, allItems);
                }
            }
        }
    }
Then each element of the list will be checked, if it is in the range of StartItem and selected TreeViewItem. If this is true the TreeViewItem will be marked as selected.


Tuesday, March 25, 2014

Sorting Evolution (7) - Sorting a WPF ListView/GridView by clicking on the header - Attached Property


Seventh generation

With the last post (Sixth Generation) we have improved our sorting mechanism that it can be easily reused in diverse projects. From that point no further changes are necessary. Nevertheless in this post we will use the developed sorting mechanism combined with Attached Property. This will remove the possibility to use the sorting mechanism within the ViewModel although sorting can be placed within XAML, only. Therefore we have to do some changes.

The Sorting class is refactored so that there is only one public method (Sort), the method SetAdorner is called from that. Determination of SortDirection is done only once in SetAdorner and is removed from Sort. The property SetData<T> and method GetView are removed. Sorting is initialized now from Attached Property (View side) not by ViewModel.

public void Sort(object columnHeader, CollectionView list)
{
    string column = SetAdorner(columnHeader);
 
    list.SortDescriptions.Clear();
    list.SortDescriptions.Add(
        new SortDescription(column, _sortDirection));
}

To create a custom Attached Property a static DependencyProperty is defined (IsSortingProperty) by using the RegisterAttached method. This method has the possibility to set a PropertyMetadata as parameter in that a PropertyChangedCallback can be defined (OnSortingPropertyChanged) by using Delegates. In the callback method an event handler is added to/removed from ListView that is called on clicking on a column header. The event handler ensures that the column is sorted. In order that the Attached Property can be used with several ListView a Dictionary is used to store a Sorting object to each ListView object. Furthermore a static Get Accessor (GetIsSorting) and a static Set Accessor (SetIsSorting) is added.

private static IDictionary<ListViewSorting> _sorting;
static SortingAttached()
{
    _sorting = new Dictionary<ListViewSorting>();
}
 
public static readonly DependencyProperty IsSortingProperty =
    DependencyProperty.RegisterAttached(
    "IsSorting",
    typeof(Boolean),
    typeof(SortingAttached),
    new PropertyMetadata(false, OnSortingPropertyChanged));
 
public static bool GetIsSorting(ListView element)
{
    return (bool)element.GetValue(IsSortingProperty);
}
 
public static void SetIsSorting(ListView element, Boolean value)
{
    element.SetValue(IsSortingProperty, value);
}
 
private static void OnSortingPropertyChanged(DependencyObject d,
    DependencyPropertyChangedEventArgs e)
{
    ListView listView = d as ListView;
 
    if (listView != null)
    {
        if (e.NewValue is bool)
        {
            if ((bool)e.NewValue)
            {
                listView.AddHandler(GridViewColumnHeader.ClickEvent,
                    new RoutedEventHandler(OnColumnHeaderClicked));
                _sorting.Add(listView, new Sorting());
            }
            else
            {
                listView.RemoveHandler(GridViewColumnHeader.ClickEvent,
                    new RoutedEventHandler(OnColumnHeaderClicked));
                _sorting.Remove(listView);
            }
        }
    }
}
 
private static void OnColumnHeaderClicked(object sender, RoutedEventArgs e)
{
    ListView listView = sender as ListView;
    if (listView == null)
    {
        return;
    }
    var sorter = _sorting[listView];
    sorter.Sort(e.OriginalSource, listView.Items);
}

In the ViewModel Sorting is removed, so it contains only data as in the Second Generation.

public ObservableCollection<ResultData> SeventhResultData { getset; }

In the XAML part of the View the Click event is removed and the Attached Property is added.

<ListView x:Name="SeventhResultData"
          sort:SortingAttached.IsSorting="True"
   ItemsSource="{Binding SeventhResultData}">
    <ListView.View>
        <GridView>
     <GridViewColumn DisplayMemberBinding="{Binding ResultNumber}">
         <GridViewColumnHeader Content="Number"
                        Padding="2,0,20,0"
          HorizontalContentAlignment="Left" />
     </GridViewColumn>
     <GridViewColumn DisplayMemberBinding="{Binding ResultOutput}">
                <GridViewColumnHeader Content="Output"
                               Padding="2,0,20,0"
                        HorizontalContentAlignment="Left" />
            </GridViewColumn>
        </GridView>
    </ListView.View>
</ListView>

In the code-behind of the View nearly all code is removed, only DataContext is there set.

With all that changes Sorting can now be done by only adding Attached Property, but cannot be triggered from ViewModel.

The source code can be downloaded from http://code.msdn.microsoft.com/windowsdesktop/Sorting-a-WPF-ListView-by-027e2303

Further Posts

  1. Sorting a WPF ListView/GridView by clicking on the header
  2. Sort Direction Indicators 
  3. Sort in ViewModel 
  4. Sort Direction Indicators with Adorners
  5. Reusability
  6. More Reusability
  7. Attached Property
  8. Behaviors (Expression Blend)