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:
You can find the sample solution in our CodeLibrary.