Many of you have probably used Telerik's TreeView or going to do so in the future. One of its many features is the TriStateMode. In this blog post I'm going to explain how TriStateMode mode is designed to work and propagate the values up and down the tree.

When in TriStateMode (IsTriStateMode=”True”) checking/unchecking an item makes the TreeView propagate the value. The value propagation actually checks/unchecks all items’ children and updates the chain of parents with appropriate value (checked, unchecked or undetermined).

CheckState vs. IsChecked

The TreeView exposes two properties used to manipulate items' check state - CheckState (of type ToggleState) and IsChecked (of type nullable Boolean). Although they look quite similar there are subtle differences between them:

1. The TreeView uses only the CheckState property to manage its TriStateMode. Meaning that CheckState property is used when propagating values up and down the TreeView.

2. Changes in CheckState are not propagated to IsChecked property.

3. CheckState property is updated when user clicks on the built in check box.

4. Built in check box is updated when CheckState property changes.

Contrary to that:

1. IsChecked property is not used in tristate value propagation and remains unchanged. The property is designed to be used with data binding and it is not updated when a tristate value propagates.

2. Changes in IsChecked are propagated to CheckState property.

3. IsChecked is not updated when user clicks on the built in check box.

4. Built in check box is updated when IsChecked property changes (because CheckState gets updated respectively the check box is updated).

Sample TreeView

Having in mind the above statements I'm going to examine the different behaviors of the TreeView when using CheckState or IsChecked property.

First I'm going to use the CheckState property and bind it to appropriate data property in the view model. The data object property is named IsChecked and raises a PropertyChanged event in order to support two way binding. The following code snippet contains the xaml definition of the tree view and the resources used. You can find rest of the code for data creation, view model and business object in the bottom of the post.

<UserControl.Resources>    
    <local:BooleanToToggleStateConverter x:Key="Bool2ToggleConverter"/>
     
    <telerik:ContainerBindingCollection x:Key="Bindings">
        <telerik:ContainerBinding PropertyName="CheckState"
            Binding="{Binding IsChecked, Mode=TwoWay, Converter={StaticResource Bool2ToggleConverter}}"/>
    </telerik:ContainerBindingCollection>
 
    <telerik:HierarchicalDataTemplate x:Key="ItemTemplate" ItemsSource="{Binding Items}"
            telerik:ContainerBinding.ContainerBindings="{StaticResource Bindings}">
        <TextBlock Text="{Binding Name}"/>
    </telerik:HierarchicalDataTemplate>    
</UserControl.Resources>
...
...
...
<telerik:RadTreeView x:Name="treeView"
        IsOptionElementsEnabled="True" IsTriStateMode="True"
        ItemsSource="{Binding .}" ItemTemplate="{StaticResource ItemTemplate}"
        telerik:ContainerBinding.ContainerBindings="{StaticResource Bindings}"/>

 

Sample 1

If you expand the whole "Data 3" branch and check item "Data 3.1" the result is going to be that the item will be checked, all its children will be checked and its parent ("Data 3") will be undetermined.

The interesting moment comes when you look at the properties of the container (RadTreeViewItem) for "Data 3.1" node. Its CheckState will be "on" but IsChecked will be "false". That is because CheckState property is used in the binding and when changed it does not update the container's (RadTreeViewItem) IsChecked property. The same is true for its children and parent items (CheckState is updated but IsChecked is false).

Sample 2

If you expand only item "Data 2" and check its second child "Data 2.2". The result is going to be that the item is checked, and its parent "Data 2" is undetermined. Again the IsChecked is false. However, if you expand item "Data 2.2" you will notice that its children are not checked.

This strange behavior is based on the fact that tristate was stopped on item “Data 2.2” and values did not propagated down to its children. If CheckState property is used in the view model binging and child containers of particular item are not created when tristate value propagation passes then the TreeView will stop the tristate propagation and leave the binding to provide values when children are created.

Sample 3

Now I'm going to use the IsChecked property instead of CheckState and change the binding declaration.

<telerik:ContainerBindingCollection x:Key="Bindings">
    <telerik:ContainerBinding PropertyName="IsChecked" Binding="{Binding IsChecked, Mode=TwoWay}"/>
</telerik:ContainerBindingCollection>

 

When using the IsChecked property in the view model binding TreeView will behave differently in previous situation (Sample 2). The key difference is that CheckState will not participate in data binding and tristate value propagation will provide necessary values when previously unrealized children items are created.

Pictures above demonstrate a comparison between “Sample 2” (in the left) and “Sample 3” (in the right).

As you can see when using CheckState in the binding the tristate value propagation is stopped at item “Data 2.2”. The container’s IsChecked property is not updated and has value of “false” for item “Data 2.2” and value of “false” for its children.

Contrary, when using IsChecked in the binding, tristate value propagation is executed upon the item’s children when they are created. The container’s IsChecked property is used in the binding and has value of “true” for item “Data 2.2” and value of “false” for its children.

What actually happens is - data object’s IsChecked changes, then container’s IsChecked is updated respectively, then container’s CheckState is updated and tristate value propagation is executed upon the children using (updating) only container’s CheckState property. This result in having value of “true” for CheckState property and value of “false” for IsChecked property for children of item “Data 2.2”.

Sample 4

If you set “IsChecked=True” to some data object before its container is created then its parent will not be affected via the tristate value propagation. If you set “IsChecked=True” to that same data object when its container has already been created then its parent will be affected via tristate functionality. In this case both properties behave the same way.

You can try it by changing the data creation function in the bottom of the post and set “IsChecked=True” for item “Data 2.1”. Notice that “Data 2.1” is checked and “Data 2” is undetermined.

TreeView does not propagate check value through parents when particular child is created. Otherwise you would witness the side effect of checking (unchecking) an item when it is expanded.

A Bit of Code

Bellow you can find rest of the code for data creation, view model and business object used in this post.

public MainPage()
{
    InitializeComponent();
    this.sampleVM = new SampleViewModel();
    this.treeView.DataContext = this.sampleVM;
}

 

public class SampleViewModel :ObservableCollection<SampleBusinessObject>
{
    public SampleViewModel()
    {
        SampleBusinessObject item;
        item = new SampleBusinessObject("Data 1");
        item.Items.Add(new SampleBusinessObject("Data 1.1"));
        item.Items[0].Items.Add(new SampleBusinessObject("Item 1.1.1"));
        item.Items[0].Items.Add(new SampleBusinessObject("Item 1.1.2"));
        item.Items[0].Items.Add(new SampleBusinessObject("Item 1.1.3"));
        item.Items.Add(new SampleBusinessObject("Data 1.2"));
        item.Items[1].Items.Add(new SampleBusinessObject("Item 1.2.1"));
        item.Items[1].Items.Add(new SampleBusinessObject("Item 1.2.2"));
        item.Items[1].Items.Add(new SampleBusinessObject("Item 1.2.3"));
        item.Items.Add(new SampleBusinessObject("Data 1.3"));
        item.Items[2].Items.Add(new SampleBusinessObject("Item 1.3.1"));
        item.Items[2].Items.Add(new SampleBusinessObject("Item 1.3.2"));
        item.Items[2].Items.Add(new SampleBusinessObject("Item 1.3.3"));
        this.Add(item);
 
        item = new SampleBusinessObject("Data 2");
        item.Items.Add(new SampleBusinessObject("Data 2.1"));
        item.Items[0].Items.Add(new SampleBusinessObject("Item 2.1.1"));
        item.Items[0].Items.Add(new SampleBusinessObject("Item 2.1.2"));
        item.Items[0].Items.Add(new SampleBusinessObject("Item 2.1.3"));
        item.Items.Add(new SampleBusinessObject("Data 2.2"));
        item.Items[1].Items.Add(new SampleBusinessObject("Item 2.2.1"));
        item.Items[1].Items.Add(new SampleBusinessObject("Item 2.2.2"));
        item.Items[1].Items.Add(new SampleBusinessObject("Item 2.2.3"));
        item.Items.Add(new SampleBusinessObject("Data 2.3"));
        item.Items[2].Items.Add(new SampleBusinessObject("Item 2.3.1"));
        item.Items[2].Items.Add(new SampleBusinessObject("Item 2.3.2"));
        item.Items[2].Items.Add(new SampleBusinessObject("Item 2.3.3"));
        this.Add(item);
 
        item = new SampleBusinessObject("Data 3");
        item.Items.Add(new SampleBusinessObject("Data 3.1"));
        item.Items[0].Items.Add(new SampleBusinessObject("Item 3.1.1"));
        item.Items[0].Items.Add(new SampleBusinessObject("Item 3.1.2"));
        item.Items[0].Items.Add(new SampleBusinessObject("Item 3.1.3"));
        item.Items.Add(new SampleBusinessObject("Data 3.2"));
        item.Items[1].Items.Add(new SampleBusinessObject("Item 3.2.1"));
        item.Items[1].Items.Add(new SampleBusinessObject("Item 3.2.2"));
        item.Items[1].Items.Add(new SampleBusinessObject("Item 3.2.3"));
        item.Items.Add(new SampleBusinessObject("Data 3.3"));
        item.Items[2].Items.Add(new SampleBusinessObject("Item 3.3.1"));
        item.Items[2].Items.Add(new SampleBusinessObject("Item 3.3.2"));
        item.Items[2].Items.Add(new SampleBusinessObject("Item 3.3.3"));
        this.Add(item);
    }
}

 

public class SampleBusinessObject :INotifyPropertyChanged
{
    public SampleBusinessObject(string name)
    {
        this.Name = name;
        this.Items = new ObservableCollection<SampleBusinessObject>();
    }
 
    public string Name { get; set; }
 
    private bool? isChecked = false;
    public bool? IsChecked
    {
        get { return this.isChecked; }
        set
        {
            if (this.isChecked != value)
            {
                this.isChecked = value;
                OnPropertyChanged("IsChecked");
            }
        }
    }
 
    public ObservableCollection<SampleBusinessObject> Items { get; set; }
 
    public event PropertyChangedEventHandler PropertyChanged;
 
    protected virtual void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

 

You can download the sample application from here.

Hope this help to understand the logic around TreeView a bit more. Of course if you have some comments or suggestions don't hesitate and drop a line.

 

Telerik TreeView for Silverlight

Telerik TreeView for WPF

And download a trial version of Telerik RadControls from here:

RadControls for Silverlight - Download Trial

RadControls for WPF - Download Trial


About the Author

Valio Stoychev

Valentin Stoychev (@ValioStoychev) for long has been part of Telerik and worked on almost every UI suite that came out of Telerik. Valio now works as a Product Manager and strives to make every customer a successful customer.

 

Related Posts

Comments

Comments are disabled in preview mode.