WPF TreeView Control With Filtering of Nodes Based on Their Levels

In this paper, I will demonstrate the process of implementing a new TreeView that provides an option to filter the nodes based on their levels. The new TreeView inherits from the Listbox and has good performance on large scale data. It is based on an idea of using custom ControlTemplate for the ListBoxItem and using ObservableCollection.

Abstract.


In this paper, I will demonstrate the process of implementing a new TreeView that provides an option to filter the nodes based on their levels. The new TreeView inherits from the Listbox and has good performance on large scale data. It is based on an idea of using custom ControlTemplate for the ListBoxItem and using ObservableCollection.


Introduction


Nobody can deny the fact that Microsoft made a big stride in the user interface technology by introducing the WPF platform. It provides better data binding, better separation of views from models, strong support for animations, styling and templating and etc. One of the amazing controls in the WPF platform is its TreeView which provides the developer a full control on the rendering process of the nodes. Using hierarchical templates, one can provide custom layouts for TreeView items. However the control faces some limitations too.

Last month, I worked on a WPF project that has a TreeView that shows thousands of nodes. My client wanted a filtering feature on the TreeView based on the Levels of nodes. In fact, he wanted a feature to select the visible levels of the tree. For example, if the tree shows a list of continents, their countries and their cities, user should be able to choose the visibility of levels. If he selects the continents and countries, the tree should only show two levels, continents and their countries. Again if he selects countries and cities, the tree should only show countries and their cities and hides continents. If he selects only cities, the tree should show the list of all cities like a Listbox. It would be a little tricky, since when user selects Level 1 for filtering, the child nodes of level 1 should be considered as childes of Level 0 nodes.

At first glance, I thought it wouldn’t be difficult and I can use the filtering provided by the CollectionView class ofItemsSource, but soon I realized that it is not related to the CollectionView and I have to do something with HierarchicalDataTemplates. Actually, I found no easy way to solve the problem. Let take a deeper look at the problem. Here is an example of MSDN on how binds a TreeView to a list:

<XmlDataProvider x:Key="myEmployeeData" XPath="/EmployeeData">

<x:XData>

<EmployeeData xmlns="">

<EmployeeInfo>

<EmployeeInfoData>Employee1</EmployeeInfoData>

<Item Type="FirstName">Jesper</Item>

<Item Type="LastName">Aaberg</Item>

<Item Type="EmployeeNumber">12345</Item>

</EmployeeInfo>

<EmployeeInfo>

<EmployeeInfoData>Employee2</EmployeeInfoData>

<Item Type="FirstName">Dominik</Item>

<Item Type="LastName">Paiha</Item>

<Item Type="EmployeeNumber">98765</Item>

</EmployeeInfo>

</EmployeeData>

</x:XData>

</XmlDataProvider>


<
HierarchicalDataTemplate DataType="EmployeeInfo"

ItemsSource ="{Binding XPath=Item}">

<TextBlock Text="{Binding XPath=EmployeeInfoData}" />

</HierarchicalDataTemplate>


<
TreeView ItemsSource="{Binding Source={StaticResource myEmployeeData},

XPath=EmployeeInfo}"/>

In the above example, the TreeView shows the xml data according to the provided HierarchicalDataTemplate. In fact, it is the structure of theHierarchicalDataTemplate that determines which levels should be visible. If I want to change the visible levels of the tree, then I should change theHierarchicalDataTemplate and its associate data models. In fact, for any combination of levels I have to provide a HierarchicalDataTemplate and based on the selected levels, one of them should be selected. If the data has four levels, we need 15 HierarchicalDataTemplates. Although, it is not impossible to create high number of templates, I think it makes the code too ugly.


I was struggling with the TreeView control for a couple of days, but finally I decided to implement my own control that is simple, but covers those issues. Last year, I saw a TreeListBox control in the Avalon controls Library (http://www.codeplex.com/AvalonControlsLib) that extends the ListBox to act as a TreeView. His control extends the ListBoxItem too. That control was written very well. It creates the child nodes as soon as one node is being expanded. That control assumes that the DataSource does not going to change.

In my scenario, the DataSource is changed in the case of changing the filtered levels. I have to do something with the DataSource too. In order to do so, instead of extending the ListBoxItem, I tried to work with existing ListBoxItem and just create customDataSource and the custom Template for ListBoxItem. The idea is based on the idea that if the model proposes a special kind of data to a Listbox, then by overriding the default ListBoxItem control template, one could have the functionality of a TreeView. In this paper, I will develop a new TreeView control that inherits from the ListBox and has the basic functionality that a TreeView control needs to have. Although it is not a replacement of the original WPF TreeView control, but in case where one needs Level filtering, our new TreeView control could be a good alternative.


Converting Data to One Dimensional


A TreeView control can display multi-dimensional data. For example, it can show a list of countries where each country has some cities and each city has some streets. On the other hand, a Listbox can only display one dimensional data. The first step in changing a Listbox to a TreeView is providing a custom data structure that turns the multi-dimensional data to one dimensional data. In order to achieve this, we introduce a new data type called TreeViewItemModel. It is as follows.


public class TreeViewItemModel : INotifyPropertyChanged, INotifyPropertyChanging

{

#region INotifyPropertyChanging

public event PropertyChangingEventHandler PropertyChanging;

protected void notifyPropertyChanging(string name)

{

if (PropertyChanging != null)

PropertyChanging(this, new PropertyChangingEventArgs(name));

}

#endregion

#region INotifyPropertyChanged

public event PropertyChangedEventHandler PropertyChanged;

protected void notifyPropertyChanged(string name)

{

if (PropertyChanged != null)

PropertyChanged(this, new PropertyChangedEventArgs(name));

}

#endregion

#region Properties

public object Data { get; set; }

public TreeViewItemModel Parent { get; private set; }

public int LevelId { get; set; }

public List<TreeViewItemModel> Children { get; set; }

public bool IsExpanded

{

get

{

return isExpanded;

}

set

{

if (isExpanded != value)

{

notifyPropertyChanging("IsExpanded");

isExpanded = value;

notifyPropertyChanged("IsExpanded");

}

}

}

private bool isExpanded;

internal bool IsInHiddenLevels = false;

public bool IsVisible

{

get

{

return isVisible;

}

internal set

{

if (isVisible != value)

{

isVisible = value;

notifyPropertyChanged("IsVisible");

}

}

}

private bool isVisible;

public DataTemplate Template

{

get

{

return template;

}

set

{

if (template != value)

{

template = value;

notifyPropertyChanged("Template");

}

}

}

private DataTemplate template;

#endregion

#region Constructor

public TreeViewItemModel(object data, TreeViewItemModel parent)

{

Children = new List<TreeViewItemModel>();

this.Data = data;

this.Parent = parent;

if (Parent != null)

{

this.LevelId = parent.LevelId + 1;

parent.Children.Add(this);

}

else

{

this.LevelId = 0;

}

}

#endregion

}


Implementing the INotifyPropertyChanged is crucial here to enable the item notify the changes. Let take a look at the properties. The Data property is the actual data that I want to show in the TreeView. The LevelId determines the Level of the item. If it is zero, then it means the item is the root of the tree. Parent points to the parent node of the item and the children property gives access to the child nodes. IsExpanded specifies whether the node has been expanded or not. IsVisible determines the visibility of the node and finally IsInHiddenLevels is used to specify the belonging the node to hidden levels. Except Data, all of the properties will be used by the Listbox to layout the items. The ItemsSource should get data as a list of TreeViewItemModel. The items in the list should be sorted in such a way that the childes are located just
under their parents. Later, I will show how we can get rides of this restriction.


Implementing Indent on Items Using Custom Control Template


The next step is changing the appearance of the listcontrol to look like a tree. It can be done by implementing a custom control template for theListBoxItem that uses the properties provided by TreeViewItemModel items. The first thing that should be changed is the indent of the items. The nodes in a tree have different indents based on their distance from the roots. The root nodes have lowest indent and their childes have more indent than roots and their grand childes have more indent than the childes and so on. The LeftMarginConverter is a custom converter that I wrote for the control. It calculates the left margin of the nodes using the levelId and Parent properties. The code of the converter is as follows:


public
class LeftMarginConverter : DependencyObject, IValueConverter

{

public LeftMarginConverter()

{

ItemLeftMargin = 10;

}

public double ItemLeftMargin { get; set; }

public Thickness DefaultThickness { get; set; }

public object Convert(object value, Type targetType, object parameter, CultureInfo culture)

{

if (value == null)

{

return Binding.DoNothing;

}

TreeViewItemModel item = (TreeViewItemModel)value;

Thickness margin = new Thickness();

if (DefaultThickness != null)

{

margin.Left = DefaultThickness.Left;

margin.Right = DefaultThickness.Right;

margin.Top = DefaultThickness.Top;

margin.Bottom = DefaultThickness.Bottom;

}

int count = 0;

Queue<TreeViewItemModel> queue = new Queue<TreeViewItemModel>();

queue.Enqueue(item);

while (queue.Count > 0)

{

TreeViewItemModel queuItem = queue.Dequeue();

foreach (TreeViewItemModel child in queuItem.Children)

{

if (!child.IsInHiddenLevels)

{

count++;

}

queue.Enqueue(child);

}

if (count > 0)

break;

}

if (count == 0)

{

margin.Left += 10;

}

while (item.Parent != null && item.LevelId > 0)

{

item = item.Parent;

if (item.IsVisible)

{

margin.Left += this.ItemLeftMargin;

}

}

return margin;

}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)

{

return Binding.DoNothing;

}

}


Converters are one of the great enhancements that WPF brought with himself to the data binding. Without a converter, changing the margin would be difficult. By providing a custom ControlTemplate for the ListBoxItem, we can use the above converter to change the default indent of items.


<
converters:LeftMarginConverter TreeViewList="{Binding ElementName=listBox}"

x:Key="marginConverter"></converters:LeftMarginConverter>

<Style TargetType="{x:Type ListBoxItem}">

<Setter Property="Template">

<Setter.Value>

<ControlTemplate TargetType="{x:Type ListBoxItem}">

<StackPanel Name="stackPanel"

Orientation="Horizontal"

Margin="{Binding Converter={StaticResource marginConverter}}">

<Border Name="Border" Padding="2" SnapsToDevicePixels="true">

<ContentPresenter />

</Border>

</StackPanel>

</ControlTemplate>

</Setter.Value>

</Setter>

</Style>

Providing Data to ItemsSource as a Custom ObservableCollection


A TreeView looks very ugly without collapse and expand buttons. The expand button expands the children of a node and the collapse button collapses its children. It means when a node is expanded or collapsed, its children should be added or removed from the list. We need a mechanism to implement such functionality and thanks to the ObservableCollection.

ObservableCollection
is a List that implements INotifyCollectionChanged interface. The INotifyCollectionChanged has just one member, an event. Objects that implement such interface can notify the changing that happened to the list via that event. Each control that has been binded to an ObservableCollection will listen to that event. The following enum illustrates the type of actions that an ObservableCollection can announce.


public
enum NotifyCollectionChangedAction

{

// Summary:

// One or more items were added to the collection.

Add = 0,

//

// Summary:

// One or more items were removed from the collection.

Remove = 1,

//

// Summary:

// One or more items were replaced in the collection.

Replace = 2,

//

// Summary:

// One or more items were moved within the collection.

Move = 3,

//

// Summary:

// The content of the collection changed dramatically.

Reset = 4,

}


Instead of using the default ObservableCollection, we implement an ObservableCollection by implementing IList and INotifyCollectionChangedinterfaces.

Our ObservableCollection provides access to only visible nodes. Visible nodes are the ones that their parents are expanded and their levels have not been filtered. In order to keep track the visibility of nodes, I add a property to the listviewitembase called IsVisible. Such property determines the visibility of items. When a node has been expanded, our ObservableCollection makes the childes of the expanded node visible and also it publishes the changes using its CollectionChanged event.


public class TreeViewListModel : IList<TreeViewItemModel>, INotifyCollectionChanged

{

#region Private members

private readonly IList<TreeViewItemModel> _items;

private IList<TreeViewItemModel> visibleItems

{

get

{

return _items.Where(c => c.IsVisible).ToList();

}

}

private Dictionary<int, TreeViewItemModel> toDeleteItems = new Dictionary<int, TreeViewItemModel>();

#endregion

#region Constructor

public TreeViewListModel(IList<TreeViewItemModel> items)

{

if (HiddenLevels == null)

HiddenLevels = new ObservableCollection<int>();

_items = items;

foreach (TreeViewItemModel item in items)

{

item.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(item_PropertyChanged);

item.PropertyChanging += new System.ComponentModel.PropertyChangingEventHandler(item_PropertyChanging);

setItemIsVisible(item);

}

}

#endregion

#region Item Visibility Changing

internal void hiddenLevelsCollectionChanged()

{

for (int i = 0; i < _items.Count; i++)

{

setItemIsVisible(_items[i]);

}

this.notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));

}

public ObservableCollection<int> HiddenLevels;

private void setItemIsVisible(TreeViewItemModel item)

{

if (HiddenLevels.Count(c => c == item.LevelId) > 0)

{

item.IsInHiddenLevels = true;

item.IsVisible = false;

return;

}

else

{

item.IsInHiddenLevels = false;

}

TreeViewItemModel parent = item.Parent;

while (parent != null)

{

if (!(parent.IsInHiddenLevels || parent.IsExpanded))

{

item.IsVisible = false;

return;

}

parent = parent.Parent;

}

item.IsVisible = true;

}

private void item_PropertyChanging(object sender, System.ComponentModel.PropertyChangingEventArgs e)

{

if (e.PropertyName == "IsExpanded")

{

TreeViewItemModel item = sender as TreeViewItemModel;

if (item.IsExpanded && item.Children.Count > 0)

{

toDeleteItems = new Dictionary<int, TreeViewItemModel>();

Queue<TreeViewItemModel> queue = new Queue<TreeViewItemModel>();

foreach (TreeViewItemModel child in item.Children)

{

queue.Enqueue(child);

}

while (queue.Count > 0)

{

TreeViewItemModel stackItem = queue.Dequeue();

if (!stackItem.IsInHiddenLevels)

{

toDeleteItems.Add(visibleItems.IndexOf(stackItem), stackItem);

}

if (stackItem.IsExpanded || stackItem.IsInHiddenLevels)

{

foreach (TreeViewItemModel child in stackItem.Children)

{

queue.Enqueue(child);

}

}

}

}

}

}

private void item_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)

{

if (e.PropertyName == "IsExpanded")

{

TreeViewItemModel item = sender as TreeViewItemModel;

if (item.IsExpanded && item.Children.Count > 0)

{

Queue<TreeViewItemModel> queue = new Queue<TreeViewItemModel>();

queue.Enqueue(item);

while (queue.Count > 0)

{

TreeViewItemModel queueItem = queue.Dequeue();

if (queueItem.IsExpanded || queueItem.IsInHiddenLevels)

{

for (int i = 0; i < queueItem.Children.Count; i++)

{

setItemIsVisible(queueItem.Children[i]);

if (queueItem.Children[i].IsVisible)

{

NotifyCollectionChangedEventArgs args = newNotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, queueItem.Children[i], visibleItems.IndexOf(queueItem.Children[i]));//, item.Children, new List<TreeViewItemModel>(), index);

notifyCollectionChanged(args);

}

queue.Enqueue(queueItem.Children[i]);

}

}

}

}

else if (!item.IsExpanded && item.Children.Count > 0)

{

try

{

for (int i = toDeleteItems.Count - 1; i >= 0; i--)

{

int key = toDeleteItems.Keys.ElementAt(i);

setItemIsVisible(toDeleteItems[key]);

NotifyCollectionChangedEventArgs args = new

NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, toDeleteItems[key], key);

notifyCollectionChanged(args);

}

}

catch

{

}

toDeleteItems.Clear();

}

}

}

#endregion

#region IList

public IEnumerator<TreeViewItemModel> GetEnumerator()

{

return visibleItems.GetEnumerator();

}

IEnumerator IEnumerable.GetEnumerator()

{

return GetEnumerator();

}

public void Add(TreeViewItemModel item)

{

throw new NotImplementedException();

}

public void Clear()

{

_items.Clear();

notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));

}

public bool Contains(TreeViewItemModel item)

{

return visibleItems.Contains(item);

}

public void CopyTo(TreeViewItemModel[] array, int arrayIndex)

{

visibleItems.CopyTo(array, arrayIndex);

}

public bool Remove(TreeViewItemModel item)

{

throw new NotImplementedException();

return false;

}

public int Count

{

get { return visibleItems.Count; }

}

public bool IsReadOnly

{

get { return _items.IsReadOnly; }

}

public int IndexOf(TreeViewItemModel item)

{

return visibleItems.IndexOf(item);

}

public void Insert(int index, TreeViewItemModel item)

{

throw new NotImplementedException();

}

public void RemoveAt(int index)

{

throw new NotImplementedException();

}

public TreeViewItemModel this[int index]

{

get { return visibleItems[index]; }

set

{

TreeViewItemModel item = visibleItems[index];

int newIndex = _items.IndexOf(item);

_items[newIndex] = value;

notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, visibleItems[index]));

}

}

#endregion

#region INotifyCollectionChanged

public event NotifyCollectionChangedEventHandler CollectionChanged;

private void notifyCollectionChanged(NotifyCollectionChangedEventArgs args)

{

if (CollectionChanged != null)

{

CollectionChanged(this, args);

}

}

#endregion

}


Expand and Collapse buttons.


The next step is adding the Expand and Collapse buttons to the itemtemplate. I use the toggle buttons. Only one of the buttons is visible at any time. For nodes that have no children none of the buttons are visible. When the Expand button is clicked, it becomes hidden and the Collapse button becomes visible and vice versa. Changing the value of IsExpanded property is the only thing that these buttons should do. By binding the IsChecked property of buttons to the IsExpandedproperty, it can be done without one line of code.

<Style TargetType="{x:Type ListBoxItem}">

<Setter Property="Template">

<Setter.Value>

<ControlTemplate TargetType="{x:Type ListBoxItem}">

<StackPanel Name="stackPanel"

PreviewMouseDown="StackPanel_PreviewMouseDown"

Orientation="Horizontal"

Margin="{Binding Converter={StaticResource marginConverter}}">

<StackPanel Orientation="Horizontal"

Visibility="{Binding Converter={StaticResource treeViewItemHeaderVisibilityConverter}}">

<ToggleButton x:Name="expandButton" IsChecked="{Binding IsExpanded, Mode=TwoWay}"

Visibility="{Binding IsExpanded, Converter={StaticResource booleanToVisibilityConverter},ConverterParameter=False}">

<ToggleButton.Template>

<ControlTemplate TargetType="ToggleButton">

<StackPanel Orientation="Horizontal">

<Image Height="10" Width="10" Source="{Binding RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type cc:ListBaseTreeView}}, Path=ExpandIcon}">

</Image>

</StackPanel>

</ControlTemplate>

</ToggleButton.Template>

</ToggleButton>

<ToggleButton x:Name="collapseButton" IsChecked="{Binding IsExpanded, Converter={StaticResourcenotBooleanConverter}, Mode=TwoWay}" Visibility="{Binding IsExpanded, Converter={StaticResource booleanToVisibilityConverter},ConverterParameter=True}">

<ToggleButton.Template>

<ControlTemplate TargetType="ToggleButton">

<StackPanel Orientation="Horizontal">

<Image Height="10" Width="10" Source="{Binding

RelativeSource={RelativeSource FindAncestor,

AncestorType={x:Type cc:ListBaseTreeView}},

Path=CollapseIcon}">

</Image>

</StackPanel>

</ControlTemplate>

</ToggleButton.Template>

</ToggleButton>

</StackPanel>

<Border Name="Border" Padding="2" SnapsToDevicePixels="true">

<ContentControl ContentTemplate="{Binding Template}" Content="{Binding Data}"></ContentControl>

</Border>

</StackPanel>

<ControlTemplate.Triggers>

<Trigger Property="IsSelected" Value="true">

<Setter TargetName="Border" Property="Background"

Value="Blue"/>

<Setter Property="Foreground"

Value="White"></Setter>

</Trigger>

</ControlTemplate.Triggers>

</ControlTemplate>

</Setter.Value>

</Setter>

</Style>

Without some kind of UI customization, a control would be useless. It is especially true for the UI of our Collapse and Expand buttons. The images of Expand and Collapse buttons should be properties that can be set outside of the control. In order to do so, we add two Dependency Properties to the control.


public ImageSource ExpandIcon

{

get

{

return (ImageSource)this.GetValue(ExpandIconProperty);

}

set

{

this.SetValue(ExpandIconProperty, value);

}

}

public static DependencyProperty ExpandIconProperty = DependencyProperty.Register(

"ExpandIcon", typeof(ImageSource), typeof(ListBaseTreeView), new PropertyMetadata());

public ImageSource CollapseIcon

{

get

{

return (ImageSource)this.GetValue(CollapseIconProperty);

}

set

{

this.SetValue(CollapseIconProperty, value);

}

}

public static DependencyProperty CollapseIconProperty = DependencyProperty.Register(

"CollapseIcon", typeof(ImageSource), typeof(ListBaseTreeView), new PropertyMetadata());


And as you may notice, the images of Expand and Collapse buttons in the ControlTemplate have been bind to those properties using RelativeSource binding.

<Image Height="10" Width="10" Source="{Binding

RelativeSource={RelativeSource FindAncestor,

AncestorType={x:Type cc:ListBaseTreeView}},

Path=CollapseIcon}">

</Image>

<Image Height="10" Width="10" Source="{Binding RelativeSource={RelativeSource FindAncestor,

AncestorType={x:Type cc:ListBaseTreeView}}, Path=ExpandIcon}">

</Image>

Traversing the Tree Using Keyboard


Most of the users prefer to use keyboard to traverse the items of a tree. In a ListBox control, only Up and Down keys are used to traverse the items, but in a TreeView, users use Right, Left and Enter keys to expand or collapse the nodes too. I handled the PreviewKeyDown event to add keyboard support. The code is simple. If user pushes the Left or Right keys, the only thing that needs to be done is changing the IsExpanded property of the selectedItem. It has been done in the ChangeIsExpandededProperty method. Please notice that I didn’t call that method directly.

Instead, I use this.Dispatcher.BeginInvoke to invoke the method. If the value of IsExpanded changed, the underlying list changes accordingly. Changing the underlying List changes the SelectedItem and it causes problem sometimes. So it is better to change that after the ListBox_PreviewKeyDown method.

private void ListBox_PreviewKeyDown(object sender, KeyEventArgs e)

{

if (e.Key == Key.Enter)

{

ListBoxTreeViewItemBase item = this.SelectedItem as ListBoxTreeViewItemBase;

if (!item.IsExpanded)

{

this.Dispatcher.BeginInvoke(new Action<ListBoxTreeViewItemBase>(ChangeIsExpandededProperty), item);

}

}

else if (e.Key == Key.Right)

{

ListBoxTreeViewItemBase item = this.SelectedItem as ListBoxTreeViewItemBase;

if (!item.IsExpanded)

{

this.Dispatcher.BeginInvoke(new Action<ListBoxTreeViewItemBase>(ChangeIsExpandededProperty), item);

}

}

else if (e.Key == Key.Left)

{

ListBoxTreeViewItemBase item = this.SelectedItem as ListBoxTreeViewItemBase;

if (item.IsExpanded)

{

this.Dispatcher.BeginInvoke(new Action<ListBoxTreeViewItemBase>(ChangeIsExpandededProperty), item);

}

else

{

if (item.Parent != null)

{

this.Dispatcher.BeginInvoke(new Action<object>(ChangeCurrentIndex), item.Parent);

}

}

}

}

private void ChangeCurrentIndex(object item)

{

this.SelectedItem = item;

ListBoxItem listBoxItem = this.ItemContainerGenerator.ContainerFromIndex(this.SelectedIndex) as ListBoxItem;

listBoxItem.Focus();

}

private void ChangeIsExpandededProperty(ListBoxTreeViewItemBase item)

{

item.IsExpanded = !item.IsExpanded;

}

Filtering the Levels


As you may notice, our ObservableCollection has a property called HiddenLevels which keeps the list of hidden levels. If it has been changed thehiddenLevelsCollectionChanged has been called to reflect the changes. It traverses the list and calls setItemIsVisible for any item. setItemIsVisible method checks the LevelId and IsExpanded of the item and its parents. If the level of the item is not in the hidden levels and also its parents have been expanded, its visibilty will be set to true.


internal void hiddenLevelsCollectionChanged()

{

for (int i = 0; i < _items.Count; i++)

{

setItemIsVisible(_items[i]);

}

this.notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));

}

private void setItemIsVisible(TreeViewItemModel item)

{

if (HiddenLevels.Count(c => c == item.LevelId) > 0)

{

item.IsInHiddenRows = true;

item.IsVisible = false;

return;

}

else

{

item.IsInHiddenRows = false;

}

TreeViewItemModel parent = item.Parent;

while (parent != null)

{

if (!(parent.IsInHiddenRows || parent.IsExpanded))

{

item.IsVisible = false;

return;

}

parent = parent.Parent;

}

item.IsVisible = true;

}


Encapsulating the Details


Our control has the full functionality now. But there is a problem. The control forces the model to provide data in specific type and specific order. Actually, it is not a best practice. The model should not depend on the view. It would be great if the control works like the default TreeView. In other words, the only thing that the user of the control should do is setting the ItemsSource and assigning a HierarchicalDataTemplate (one that contains all levels). In fact, the control should hide the detail and encapsulates the creation of ObservableCollection. The best idea could be capturing the change of ItemsSource in theOnPropertyChanged method and override the default behavior.


The first step is providing a dependencyProperty for the HierarchicalDataTemplate.


public
HierarchicalDataTemplate HierarchicalDataTemplate

{

get

{

return (HierarchicalDataTemplate)this.GetValue(HierarchicalDataTemplateProperty);

}

set

{

this.SetValue(HierarchicalDataTemplateProperty, value);

}

}

public static DependencyProperty HierarchicalDataTemplateProperty =DependencyProperty.Register("HierarchicalDataTemplate", typeof(HierarchicalDataTemplate), typeof(ListBaseTreeView), newPropertyMetadata());


The next step, is overriding the onpropertyChanged method and overriding the default behavior.

protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)

{

if (e.Property.Name == "ItemsSource")

{

if (HierarchicalDataTemplate != null)

{

if (!(e.NewValue is ListBoxTreeViewList))

{

ItemsSource = getItems(e.NewValue as IEnumerable, HierarchicalDataTemplate);

}

else

{

base.OnPropertyChanged(e);

}

}

}

else if (e.Property.Name == "HiddenLevels")

{

ListBoxTreeViewList list = ItemsSource as ListBoxTreeViewList;

if (list != null)

{

list.HiddenLevels = e.NewValue as ObservableCollection<int>;

list.hiddenLevelsCollectionChanged();

}

}

else

{

base.OnPropertyChanged(e);

}

}

And the last step is generating ListBoxTreeViewList from given ItemsSource.


private
ListBoxTreeViewList getItems(IEnumerable itemsSource, HierarchicalDataTemplate hierarchicalDataTemplate)

{

Queue<ListBoxTreeViewItemBase> rootElementes = new Queue<ListBoxTreeViewItemBase>();

Queue<ListBoxTreeViewItemBase> queue = new Queue<ListBoxTreeViewItemBase>();

foreach (object data in itemsSource)

{

ListBoxTreeViewItemBase item = new ListBoxTreeViewItemBase(data, null);

item.Template = hierarchicalDataTemplate;

queue.Enqueue(item);

rootElementes.Enqueue(item);

}

while (queue.Count > 0)

{

ListBoxTreeViewItemBase item = queue.Dequeue();

HierarchicalDataTemplate template = item.Template as HierarchicalDataTemplate;

if (template != null && template.ItemsSource != null)

{

string path = (template.ItemsSource as Binding).Path.Path;

IEnumerable enumerable = (IEnumerable)item.Data.GetType().GetProperty(path).GetValue(item.Data, null);

foreach (object data in enumerable)

{

ListBoxTreeViewItemBase childItem = new ListBoxTreeViewItemBase(data, item);

childItem.Template = template.ItemTemplate;

queue.Enqueue(childItem);

}

}

}

List<ListBoxTreeViewItemBase> list = new List<ListBoxTreeViewItemBase>();

while (rootElementes.Count > 0)

{

ListBoxTreeViewItemBase item = rootElementes.Dequeue();

insert(list, item);

}

return new ListBoxTreeViewList(list);

}

private void insert(List<ListBoxTreeViewItemBase> list, ListBoxTreeViewItemBase item)

{

list.Add(item);

foreach (ListBoxTreeViewItemBase child in item.Children)

{

insert(list, child);

}

}

In the first while loop, all of the items will be turned to ListBoxTreeViewItemBase. In the second while loop, all of the nodes will be added to a list. I used a recursive function in the second loop, since it makes the code simpler. Now, we have a control that hides the detail. The only issue which is negligible is related to the SelectedItem property which returns TreeViewItemModel, but it is not critical since the real data is located in its Data property.


Using The Control


Using the control is similar to using the original TreeView control. You should just assign a BindingSource to the ItemsSource property and provide a value for itsHierarchicalTemplate.


<controls:ListBaseTreeView HiddenLevels="{Binding Hiddens}" HierarchicalDataTemplate="{StaticResourceleagueTemplate}" SelectedItem="{Binding SelectedItem}" Grid.Row="1" CollapseIcon="{StaticResource closeImage}"ExpandIcon="{StaticResource openImage}" ItemsSource="{Binding MyList}" ></controls:ListBaseTreeView>

Conclusion


In this paper, I showed the process of implementing a new TreeView that inherits from the ListBox. The new TreeView can filter the items based on their levels. Since it inherits from a Listbox, it has a better performance than the out of the box TreeView of the Framework; however our control has one limitation too. One cannot change its layout. In addition to that, its SelectedItem returns an object of type TreeViewItemModel. The real selected data is located in the Data property of TreeViewItemModel.


Download the code here.

By Siyamand Ayubi   Popularity  (8280 Views)