Implement a Tri-State CheckBox logic using MVVM

This tutorial will guide you through the process of implementing a 'tri-state' CheckBox functionality in the RadTreeView using MVVM.

The RadTreeView control supports check boxes/radio buttons elements next to each item out-of-the-box. However, their 'tri-state' logic implementation is designed to work when the RadTreeView.Items collection is populated with RadTreeViewItems. Basically it will work as expected when the RadTreeView is declaratively populated or its Items collection is populated in code-behind. However, the RadTreeView control is mostly used in databinding scenarios following the MVVM pattern. And if your applicaiton requirements include a 'tri-state' check box logic, then it's best to define a CheckBox control inside the RadTreeViewItem's DataTemplates and implement the 'tri-state' logic entirely in the view models.

  • Let's start with defining a sample view model for the RadTreeViewItems. It only contains a name, collection of children items and a checked property:

        using System; 
        using System.Collections.ObjectModel; 
        using Telerik.Windows.Controls; 
        using System.Linq; 
     
        namespace TreeViewMVVMCheckBoxSample.ViewModels 
        { 
            public class CategoryViewModel : ViewModelBase 
            { 
                private string _name; 
                private bool? _isChecked; 
     
                private ObservableCollection<CategoryViewModel> _subCategories = null; 
     
                public string Name 
                { 
                    get 
                    { 
                        return this._name; 
                    } 
                    set 
                    { 
                        this._name = value; 
                    } 
                } 
                public bool? IsChecked 
                { 
                    get 
                    { 
                        return this._isChecked; 
                    } 
                    set 
                    { 
                        if (this._isChecked != value) 
                        { 
                            this._isChecked = value; 
                            OnPropertyChanged("IsChecked"); 
                        } 
                    } 
                } 
     
                public ObservableCollection<CategoryViewModel> SubCategories 
                { 
                    get 
                    { 
                        if (this._subCategories == null) 
                        { 
                            this._subCategories = new ObservableCollection<CategoryViewModel>(); 
                        } 
                        return this._subCategories; 
                    } 
                } 
            } 
        } 
    
        Imports System.Collections.ObjectModel 
        Imports Telerik.Windows.Controls 
     
        Namespace TreeViewMVVMCheckBoxSample.ViewModels 
            Public Class CategoryViewModel 
                Inherits ViewModelBase 
                Private _name As String 
                Private _isChecked As Boolean? 
     
                Private _subCategories As ObservableCollection(Of CategoryViewModel) = Nothing 
     
                Public Property Name() As String 
                    Get 
                        Return Me._name 
                    End Get 
                    Set(ByVal value As String) 
                        Me._name = value 
                    End Set 
                End Property 
                Public Property IsChecked() As Boolean? 
                    Get 
                        Return Me._isChecked 
                    End Get 
                    Set(ByVal value As Boolean?) 
                        If Not Me._isChecked.Equals(value) Then 
                            Me._isChecked = value 
                            OnPropertyChanged("IsChecked") 
                        End If 
                    End Set 
                End Property 
     
                Public ReadOnly Property SubCategories() As ObservableCollection(Of CategoryViewModel) 
                    Get 
                        If Me._subCategories Is Nothing Then 
                            Me._subCategories = New ObservableCollection(Of CategoryViewModel)() 
                        End If 
                        Return Me._subCategories 
                    End Get 
                End Property 
            End Class 
        End Namespace 
    

    Please note that the CategoryViewModel class inherits from the Telerik.Windows.Controls.ViewModelBase class. It provides support for property change notifications and we need to notify the RadTreeViewItems when the IsChecked property is changed.

  • Now let's extend that sample model to include our 'tri-state' logic. Firstly, in order to update the checked state of the parent items, each item will have to keep a reference of its parent item.

        private CategoryViewModel parentItem; 
    
        Private parentItem As CategoryViewModel 
    
  • Then we need to implement the logic that determines the checked state of each item. For that purpose we have to traverse the children colleciton of a checked item as well as to find the checked state in which its parent item should be set.

    • Let's create a method traversing the children collection of an item:

              private void UpdateChildrenCheckState() 
              { 
                  foreach (var item in this.SubCategories) 
                  { 
                      if (this.IsChecked != null) 
                      { 
                          item.IsChecked = this.IsChecked; 
                      } 
                  } 
              } 
      
              Private Sub UpdateChildrenCheckState() 
                  For Each item In Me.SubCategories 
                      If Me.IsChecked IsNot Nothing Then 
                          item.IsChecked = Me.IsChecked 
                      End If 
                  Next item 
              End Sub 
      
    • We can also create a method that updates the checked state of the parent item. In order to simplify the code, we can use a lambda function to count the number of the checked children of the parent item. If this number indicates that all its children are checked, we can set the parent item checked state to checked, if the count of its checked children is 0, then we need to uncheck it. In all other cases, its state should stay indeterminate.

              private bool? DetermineCheckState() 
              { 
                  bool allChildrenChecked = this.SubCategories.Count(x => x.IsChecked == true) == this.SubCategories.Count; 
                  if (allChildrenChecked) 
                  { 
                      return true; 
                  } 
       
                  bool allChildrenUnchecked = this.SubCategories.Count(x => x.IsChecked == false) == this.SubCategories.Count; 
                  if (allChildrenUnchecked) 
                  { 
                      return false; 
                  } 
       
                  return null; 
              }                
      
              Private Function DetermineCheckState() As Boolean? 
                  Dim allChildrenChecked As Boolean = Me.SubCategories.LongCount(Function(x) x.IsChecked.Equals(True)) = Me.SubCategories.Count 
                  If allChildrenChecked Then 
                      Return True 
                  End If 
       
                  Dim allChildrenUnchecked As Boolean = Me.SubCategories.LongCount(Function(x) x.IsChecked.Equals(False)) = Me.SubCategories.Count 
                  If allChildrenUnchecked Then 
                      Return False 
                  End If 
       
                  Return Nothing 
              End Function 
      
  • We need to call both methods when the checked state of each item is changed. That basically means that we need to call them when the IsChecked property value is changed:

        public bool? IsChecked 
        { 
            get 
            { 
                return this._isChecked; 
            } 
            set 
            { 
                if (this._isChecked != value) 
                { 
                    this._isChecked = value; 
                    this.UpdateCheckState(); 
                    OnPropertyChanged("IsChecked"); 
                } 
            } 
        } 
     
        private void UpdateCheckState() 
        { 
            // update all children: 
            if (this.SubCategories.Count != 0) 
            { 
                this.UpdateChildrenCheckState(); 
            } 
            //update parent item 
            if (this.parentItem != null) 
            { 
                bool? parentIsChecked = this.parentItem.DetermineCheckState(); 
                this.parentItem.IsChecked = parentIsChecked; 
            } 
        } 
    
        Public Property IsChecked() As Boolean? 
            Get 
                Return Me._isChecked 
            End Get 
            Set(ByVal value As Boolean?) 
                If Not Me._isChecked.Equals(value) Then 
                    Me._isChecked = value 
                    Me.UpdateCheckState() 
                    OnPropertyChanged("IsChecked") 
                End If 
            End Set 
        End Property 
        Private Sub UpdateCheckState() 
            ' update all children: ' 
            If Me.SubCategories.Count <> 0 Then 
                Me.UpdateChildrenCheckState() 
            End If 
            'update parent item ' 
            If Me.parentItem IsNot Nothing Then 
                Dim parentIsChecked? As Boolean = Me.parentItem.DetermineCheckState() 
                Me.parentItem.IsChecked = parentIsChecked 
     
            End If 
        End Sub 
    
  • Now our CategoryViewModel logic is almost complete. However, if you take a closer look at the IsChecked property setter implementation, you will notice that the UpdateCheckState() method will cause the setter to be executed multiple times for the same item. This is why we'll have to implement a reentrancy check:

        private bool reentrancyCheck = false; 
        public bool? IsChecked 
        { 
            get 
            { 
                return this._isChecked; 
            } 
            set 
            { 
                if (this._isChecked != value) 
                { 
                    if (reentrancyCheck) 
                        return; 
                    this.reentrancyCheck = true; 
                    this._isChecked = value; 
                    this.UpdateCheckState(); 
                    OnPropertyChanged("IsChecked"); 
                    this.reentrancyCheck = false; 
                } 
            } 
        } 
    
        Private reentrancyCheck As Boolean = False 
        Public Property IsChecked() As Boolean? 
            Get 
                Return Me._isChecked 
            End Get 
            Set(ByVal value As Boolean?) 
                If Not Me._isChecked.Equals(value) Then 
                    If reentrancyCheck Then 
                        Return 
                    End If 
                    Me.reentrancyCheck = True 
                    Me._isChecked = value 
                    Me.UpdateCheckState() 
                    OnPropertyChanged("IsChecked") 
                    Me.reentrancyCheck = False 
                End If 
            End Set 
        End Property 
    
  • So finally the CategoryViewModel looks like that:

        using System; 
        using System.Collections.ObjectModel; 
        using Telerik.Windows.Controls; 
        using System.Linq; 
     
        namespace TreeViewMVVMCheckBoxSample.ViewModels 
        { 
            public class CategoryViewModel : ViewModelBase 
            { 
                private string _name; 
                private bool? _isChecked; 
                private bool reentrancyCheck = false; 
                private CategoryViewModel parentItem; 
     
                private ObservableCollection<CategoryViewModel> _subCategories = null; 
     
                public string Name 
                { 
                    get 
                    { 
                        return this._name; 
                    } 
                    set 
                    { 
                        this._name = value; 
                    } 
                } 
                public bool? IsChecked 
                { 
                    get 
                    { 
                        return this._isChecked; 
                    } 
                    set 
                    { 
                        if (this._isChecked != value) 
                        { 
                            if (reentrancyCheck) 
                                return; 
                            this.reentrancyCheck = true; 
                            this._isChecked = value; 
                            this.UpdateCheckState(); 
                            OnPropertyChanged("IsChecked"); 
                            this.reentrancyCheck = false; 
                        } 
                    } 
                } 
     
                public ObservableCollection<CategoryViewModel> SubCategories 
                { 
                    get 
                    { 
                        if (this._subCategories == null) 
                        { 
                            this._subCategories = new ObservableCollection<CategoryViewModel>(); 
                        } 
                        return this._subCategories; 
                    } 
                } 
     
                public CategoryViewModel(CategoryViewModel parent) 
                { 
                    this.parentItem = parent; 
                } 
     
                private void UpdateCheckState() 
                { 
                    // update all children: 
                    if (this.SubCategories.Count != 0) 
                    { 
                        this.UpdateChildrenCheckState(); 
                    } 
                    //update parent item 
                    if (this.parentItem != null) 
                    { 
                        bool? parentIsChecked = this.parentItem.DetermineCheckState(); 
                        this.parentItem.IsChecked = parentIsChecked; 
     
                    } 
                } 
     
                private void UpdateChildrenCheckState() 
                { 
                    foreach (var item in this.SubCategories) 
                    { 
                        if (this.IsChecked != null) 
                        { 
                            item.IsChecked = this.IsChecked; 
                        } 
                    } 
                } 
     
                private bool? DetermineCheckState() 
                { 
                    bool allChildrenChecked = this.SubCategories.Count(x => x.IsChecked == true) == this.SubCategories.Count; 
                    if (allChildrenChecked) 
                    { 
                        return true; 
                    } 
     
                    bool allChildrenUnchecked = this.SubCategories.Count(x => x.IsChecked == false) == this.SubCategories.Count; 
                    if (allChildrenUnchecked) 
                    { 
                        return false; 
                    } 
     
                    return null; 
                } 
            } 
        } 
    
        Imports System.Collections.ObjectModel 
        Imports Telerik.Windows.Controls 
     
        Namespace TreeViewMVVMCheckBoxSample.ViewModels 
            Public Class CategoryViewModel 
                Inherits ViewModelBase 
                Private _name As String 
                Private _isChecked As Boolean? 
                Private reentrancyCheck As Boolean = False 
                Private parentItem As CategoryViewModel 
     
                Private _subCategories As ObservableCollection(Of CategoryViewModel) = Nothing 
     
                Public Property Name() As String 
                    Get 
                        Return Me._name 
                    End Get 
                    Set(ByVal value As String) 
                        Me._name = value 
                    End Set 
                End Property 
                Public Property IsChecked() As Boolean? 
                    Get 
                        Return Me._isChecked 
                    End Get 
                    Set(ByVal value As Boolean?) 
                        If Not Me._isChecked.Equals(value) Then 
                            If reentrancyCheck Then 
                                Return 
                            End If 
                            Me.reentrancyCheck = True 
                            Me._isChecked = value 
                            Me.UpdateCheckState() 
                            OnPropertyChanged("IsChecked") 
                            Me.reentrancyCheck = False 
                        End If 
                    End Set 
                End Property 
     
                Public ReadOnly Property SubCategories() As ObservableCollection(Of CategoryViewModel) 
                    Get 
                        If Me._subCategories Is Nothing Then 
                            Me._subCategories = New ObservableCollection(Of CategoryViewModel)() 
                        End If 
                        Return Me._subCategories 
                    End Get 
                End Property 
     
                Public Sub New(ByVal parent As CategoryViewModel) 
                    Me.parentItem = parent 
                End Sub 
     
                Private Sub UpdateCheckState() 
                    ' update all children: ' 
                    If Me.SubCategories.Count <> 0 Then 
                        Me.UpdateChildrenCheckState() 
                    End If 
                    'update parent item ' 
                    If Me.parentItem IsNot Nothing Then 
                        Dim parentIsChecked? As Boolean = Me.parentItem.DetermineCheckState() 
                        Me.parentItem.IsChecked = parentIsChecked 
     
                    End If 
                End Sub 
     
                Private Sub UpdateChildrenCheckState() 
                    For Each item In Me.SubCategories 
                        If Me.IsChecked IsNot Nothing Then 
                            item.IsChecked = Me.IsChecked 
                        End If 
                    Next item 
                End Sub 
     
                Private Function DetermineCheckState() As Boolean? 
                    Dim allChildrenChecked As Boolean = Me.SubCategories.LongCount(Function(x) x.IsChecked.Equals(True)) = Me.SubCategories.Count 
                    If allChildrenChecked Then 
                        Return True 
                    End If 
     
                    Dim allChildrenUnchecked As Boolean = Me.SubCategories.LongCount(Function(x) x.IsChecked.Equals(False)) = Me.SubCategories.Count 
                    If allChildrenUnchecked Then 
                        Return False 
                    End If 
     
                    Return Nothing 
                End Function 
            End Class 
        End Namespace 
    
  • As the items ViewModel is ready, we can create a MainViewModel to define a collection of CategoryViewModel objects that will be used as the RadTreeView.ItemsSource.

        using System; 
        using System.Collections.ObjectModel; 
     
        namespace TreeViewMVVMCheckBoxSample.ViewModels 
        { 
            public class MainViewModel 
            { 
                public ObservableCollection<CategoryViewModel> Categories { get; set; } 
     
                public MainViewModel() 
                { 
                    Categories = new ObservableCollection<CategoryViewModel>(); 
     
                    CategoryViewModel beverages = new CategoryViewModel(null); 
                    beverages.Name = "Bevereges"; 
     
                    for (int i = 0; i < 5; i++) 
                    { 
                        CategoryViewModel prod = new CategoryViewModel(beverages) 
                        { 
                            Name = String.Format("Beverage {0}", i), 
                            IsChecked = false 
                        }; 
     
                        for (int j = 0; j < 3; j++) 
                        { 
                            prod.SubCategories.Add(new CategoryViewModel(prod) 
                        { 
                            Name = String.Format("SubBeverage {0}.{1}", i, j), 
                            IsChecked = false 
                        }); 
                        } 
                        beverages.SubCategories.Add(prod); 
                    } 
                    Categories.Add(beverages); 
     
     
                    CategoryViewModel confections = new CategoryViewModel(null); 
                    confections.Name = "Confections"; 
                    for (int i = 0; i < 7; i++) 
                    { 
                        confections.SubCategories.Add(new CategoryViewModel(confections) 
                        { 
                            Name = String.Format("Confection {0}", i), 
                            IsChecked = false 
                        }); 
                    } 
                    Categories.Add(confections); 
     
                    CategoryViewModel condiments = new CategoryViewModel(null); 
                    condiments.Name = "Condiments"; 
                    for (int i = 0; i < 3; i++) 
                    { 
                        condiments.SubCategories.Add(new CategoryViewModel(condiments) 
                        { 
                            Name = String.Format("Condiment {0}", i), 
                            IsChecked = false 
                        }); 
                    } 
                    Categories.Add(condiments); 
                } 
            } 
        } 
    
        Imports System.Collections.ObjectModel 
     
        Namespace TreeViewMVVMCheckBoxSample.ViewModels 
            Public Class MainViewModel 
                Public Property Categories() As ObservableCollection(Of CategoryViewModel) 
     
                Public Sub New() 
                    Categories = New ObservableCollection(Of CategoryViewModel)() 
     
                    Dim beverages As New CategoryViewModel(Nothing) 
                    beverages.Name = "Bevereges" 
     
                    For i As Integer = 0 To 4 
                        Dim prod As New CategoryViewModel(beverages) With {.Name = String.Format("Beverage {0}", i), .IsChecked = False} 
     
                        For j As Integer = 0 To 2 
                            prod.SubCategories.Add(New CategoryViewModel(prod) With {.Name = String.Format("SubBeverage {0}.{1}", i, j), .IsChecked = False}) 
                        Next j 
                        beverages.SubCategories.Add(prod) 
                    Next i 
                    Categories.Add(beverages) 
     
     
                    Dim confections As New CategoryViewModel(Nothing) 
                    confections.Name = "Confections" 
                    For i As Integer = 0 To 6 
                        confections.SubCategories.Add(New CategoryViewModel(confections) With {.Name = String.Format("Confection {0}", i), .IsChecked = False}) 
                    Next i 
                    Categories.Add(confections) 
     
                    Dim condiments As New CategoryViewModel(Nothing) 
                    condiments.Name = "Condiments" 
                    For i As Integer = 0 To 2 
                        condiments.SubCategories.Add(New CategoryViewModel(condiments) With {.Name = String.Format("Condiment {0}", i), .IsChecked = False}) 
                    Next i 
                    Categories.Add(condiments) 
     
                End Sub 
            End Class 
        End Namespace 
    
  • Finally we need to set up the RadTreeView control and its ItemTemplate. Please note that we won't use the RadTreeView check-box support, but instead we will define a CheckBox in the ItemTemplate of the control.

        <UserControl.DataContext> 
            <vm:MainViewModel /> 
        </UserControl.DataContext> 
        <Grid x:Name="LayoutRoot"> 
            <telerik:RadTreeView Margin="5" ItemsSource="{Binding Categories}" Padding="5"> 
                <telerik:RadTreeView.ItemTemplate> 
                    <telerik:HierarchicalDataTemplate ItemsSource="{Binding SubCategories}"> 
                        <StackPanel Orientation="Horizontal"> 
                            <CheckBox IsChecked="{Binding IsChecked, Mode=TwoWay}" telerik:StyleManager.Theme="Office_Black" /> 
                            <TextBlock VerticalAlignment="Center" Text="{Binding Name}" /> 
                        </StackPanel> 
                    </telerik:HierarchicalDataTemplate> 
                </telerik:RadTreeView.ItemTemplate> 
            </telerik:RadTreeView> 
        </Grid> 
    

    The telerik alias represents the telerik namespace: xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation" The vm alias represents the viewmodels local namespace. For example: xmlns:vm="clr-namespace:TreeViewMVVMCheckBoxSample.ViewModels"

  • When you run this project, you should see the following output: Rad Tree View-How To-Tri State-MVVM

You can find the sample solution in our CodeLibrary.

See Also

In this article