C# Triple State Checkbox Tree-View

C# Triple State Tree-View

This post show how to setup an MVVM Tree view with Triple State Checkboxes.

Solution setup

Within Visual Studio create the Following WPF Project, folder structure and files.

Visual Studio Solution

Models

We will need two simple models for this example.

Thing Model

The Thing Model consists of a Parent property, Child List Property and Name Property, plus a couple of helper routines for setting the parent and child property’s.

//ThingModel.cs
using System.Collections.Generic;

namespace treeview.Models
{
    public class ThingModel
    {
        public ThingModel Parent { get; set; }
        public List<ThingModel> Children { get; set; }
        public string Name { get; set; }

        public ThingModel(string name)
        {
            Name = name;
        }

        public ThingModel(string name, List<ThingModel> children)
        {
            Name = name;
            Children = children;
        }

        public void SetParent(ThingModel parent)
        {
            Parent = parent;
        }

        public void SetChild(List<ThingModel> children)
        {
            Children = children;
        }
    }
}

Things Model

The Things Model is much simpler and just contains a list of Thing Model property.

//ThingsModel.cs
using System.Collections.Generic;

namespace treeview.Models
{
    public class ThingsModel
    {
        public List<ThingModel> ThingModels { get; set; }
    }
}

View Models

The View Models drive the application and contain the required business logic.

View Model Base

The View Model Base will be inherited by all the view models and will supply the “OnPropertyChange” method required by WPF to know when property’s are updated.

//ViewModelBase.cs

using System.ComponentModel;

namespace treeview.ViewModels
{
    public class ViewModelBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

I Thing View Model

This interface is required by the XAML markup language when defining the tree view Hierarchical Data Template.

//IThingViewModel.cs

using System.Collections.Generic;
using System.Windows.Input;
using treeview.Models;

namespace treeview.ViewModels
{
    public interface IThingViewModel
    {
        bool? AllChildrenChecked { get; }
        bool? AllChildrenUnChecked { get; }
        bool HasChildren { get; }
        bool HasParent { get; }
        bool? IsChecked { get; set; }
        bool IsRoot { get; }
        bool IsLeaf { get; }

        string Name { get; }
        string State { get; }

        ThingModel Parent { get; }      
        ThingViewModel ViewModelParent { get; }
        List<ThingModel> Children { get; }
        List<ThingViewModel> ViewModelChildren { get; }

        ICommand CheckboxCommand { get; }

        void CheckAllChildren();
        void CheckAllParents();
        void UpdateParentIsChecked(bool? isChecked);
    }
}

ThingViewModel

The Thing View Model is the main guts of the application and contains all the required business logic to drive the Tree view and the checkbox status.

//ThingViewModel.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Input;
using treeview.Commands;
using treeview.Models;

namespace treeview.ViewModels
{
    public class ThingViewModel : ViewModelBase, IThingViewModel
    {
        ThingModel _thingModel;
        
        public Guid guid;

        private bool? _isChecked = false;
        public bool? IsChecked
        {
            get
            {
                return _isChecked;
            }
            set
            {
                _isChecked = value;
                OnPropertyChanged(nameof(IsChecked));
                OnPropertyChanged(nameof(State));
            }
        }

        public bool? AllChildrenChecked
        {
            get
            {
                if (ViewModelChildren != null)
                {
                    bool? returnBool = false;
                    bool? checkedBool = false;
                    var y = from x in ViewModelChildren
                            where x.IsChecked == true
                            select x;
                    checkedBool = (y.Count() == ViewModelChildren.Count()) ? true : false;

                    bool? nullBool = false;
                    var w = from z in ViewModelChildren
                            where z.IsChecked == null
                            select z;
                    nullBool = (w.Count() == ViewModelChildren.Count()) ? true : false;

                    returnBool = (nullBool == true) ? nullBool : checkedBool;

                    return returnBool;
                }
                return false;
            }
        }
        public bool? AllChildrenUnChecked
        {
            get
            {
                if (ViewModelChildren != null)
                {
                    bool? returnBool = false;
                    bool? uncheckedBool = false;

                    var y = from x in ViewModelChildren
                            where x.IsChecked == false
                            select x;
                    uncheckedBool=(y.Count() == ViewModelChildren.Count()) ? true : false;

                    bool? nullBool = false;
                    var w = from z in ViewModelChildren
                            where z.IsChecked == null
                            select z;
                    nullBool = (w.Count() == ViewModelChildren.Count()) ? true : false;

                    returnBool = (nullBool == false) ? uncheckedBool: nullBool;

                    return returnBool;
                }
                return false;
            }
        }
        public bool IsRoot { get; private set; } = false;
        public bool IsLeaf
        {
            get
            {
                return (HasChildren==false) ?true:false;
            }
        }
        public bool HasChildren
        {
            get
            {
                return (ViewModelChildren == null) ? false : true;
            }
        }
        public bool HasParent
        {
            get
            {
                return (ViewModelParent == null) ? false : true;
            }
        }

        public string State
        {
            get
            {
                if (IsChecked == null)
                {
                    return "Null";
                }
                else if (IsChecked == true)
                {
                    return "True";
                }
                else
                {
                    return "False";
                }
            }
        }
        public string Name => _thingModel.Name;

        public ThingModel Parent => _thingModel.Parent;
        public List<ThingModel> Children => _thingModel.Children;

        private List<ThingViewModel> _viewModelChildren = null;
        public List<ThingViewModel> ViewModelChildren
        {
            get
            {
                if (_viewModelChildren == null)
                {
                    _viewModelChildren = GetViewModelChildren();
                    return _viewModelChildren;
                }
                return _viewModelChildren;
            }
        }

        private ThingViewModel _viewModelParent = null;
        public ThingViewModel ViewModelParent
        {
            get
            {
                if (_viewModelParent == null)
                {
                    _viewModelParent = GetViewModelParent();
                    return _viewModelParent;
                }
                else
                {
                    return _viewModelParent;
                }
            }
        }
        public ICommand CheckboxCommand { get; }

        public ThingViewModel(ThingModel thingModel)
        {
            _thingModel = thingModel;
            CheckboxCommand = new CheckboxCommand(this);
            guid = Guid.NewGuid();
        }
        public ThingViewModel(ThingModel thingModel, bool isRoot)
        {
            _thingModel = thingModel;
            IsRoot = isRoot;
            CheckboxCommand = new CheckboxCommand(this);
            guid = Guid.NewGuid();
        }
        public ThingViewModel(ThingModel thingModel, ThingViewModel thingViewModel)
        {
            _thingModel = thingModel;
            _viewModelParent = thingViewModel;
            CheckboxCommand = new CheckboxCommand(this);
            guid = Guid.NewGuid();
        }
            
        
        public void UpdateParentIsChecked(bool? isChecked)
        {
            ViewModelParent.IsChecked = isChecked;
        }
        public void CheckAllChildren()
        {
            IsChecked = (HasChildren) ? null : true;
            if (HasChildren == true)
            {
                foreach (ThingViewModel thingViewModel in ViewModelChildren)
                {
                    thingViewModel.IsChecked = true;
                    thingViewModel.CheckAllChildren();
                }
            }
        }
        public void CheckAllParents()
        {
            if (HasParent == true && IsRoot == false)
            {
                ThingViewModel thingViewModel = ViewModelParent;

                if (ViewModelParent.GetViewModelChildren() != null)
                {
                    bool? State = (thingViewModel.AllChildrenChecked == true) ? null : true;
                    thingViewModel.IsChecked = State;
                    thingViewModel.CheckAllParents();
                }
            }
        }
        public void UnCheckAllChildren()
        {
            IsChecked = false;
            if (HasChildren == true)
            {
                foreach (ThingViewModel thingViewModel in ViewModelChildren)
                {
                    thingViewModel.IsChecked = false;
                    thingViewModel.UnCheckAllChildren();
                }
            }
        }
        public void UnCheckAllParents()
        {
            if (HasParent == true && IsRoot == false)
            {
                ThingViewModel thingViewModel = ViewModelParent;

                if (ViewModelParent.GetViewModelChildren() != null)
                {
                    bool? State = (thingViewModel.AllChildrenUnChecked == true) ? false : true;
                    if(thingViewModel.AllChildrenUnChecked == false)
                    {
                        State = true;
                    }
                    thingViewModel.IsChecked = State;
                    thingViewModel.UnCheckAllParents();
                }
            }
        }
        
        private List<ThingViewModel> GetViewModelChildren()
        {
            List<ThingViewModel> thingViewModels = new List<ThingViewModel>();
            if (Children != null)
            {
                foreach (ThingModel thingModel in Children)
                {
                    thingViewModels.Add(new ThingViewModel(thingModel, this));
                }
                return thingViewModels;
            }
            return null;
        }
        private ThingViewModel GetViewModelParent()
        {
            return new ThingViewModel(Parent);
        }
    }
}

Things View Model

The Things View Model is supplying the data for the Tree View and would normally come from an external data source. For this example we are hard coding the data.

//ThingsViewModel.cs

using System.Collections.Generic;
using treeview.Models;

namespace treeview.ViewModels
{
    public class ThingsViewModel: ViewModelBase
    {
        public ThingModel GetAllThings()
        {
            ThingModel EnvPath1 = new ThingModel("Gary");
            ThingModel EnvPath2 = new ThingModel("Mike");
            ThingModel EnvPath3 = new ThingModel("Peter" , new List<ThingModel>() { EnvPath1, EnvPath2 });
            EnvPath1.SetParent(EnvPath3);
            EnvPath2.SetParent(EnvPath3);

            ThingModel EnvPath4 = new ThingModel("Lucy");
            ThingModel EnvPath5 = new ThingModel("Shelley");
            ThingModel EnvPath6 = new ThingModel("Claire", new List<ThingModel>() { EnvPath4, EnvPath5 });
            EnvPath4.SetParent(EnvPath6);
            EnvPath5.SetParent(EnvPath6);

            ThingModel EnvPath7 = new ThingModel("Jake" , new List<ThingModel>() { EnvPath3, EnvPath6 });
            EnvPath3.SetParent(EnvPath7);
            EnvPath6.SetParent(EnvPath7);


            ThingModel EnvPath8 = new ThingModel("AAAA");
            ThingModel EnvPath9 = new ThingModel("BBBBB");
            ThingModel EnvPath10 = new ThingModel("CCCCC", new List<ThingModel>() { EnvPath8, EnvPath9 });
            EnvPath8.SetParent(EnvPath10);
            EnvPath9.SetParent(EnvPath10);

            ThingModel EnvPath11 = new ThingModel("DDDDD");
            ThingModel EnvPath12 = new ThingModel("EEEEE");
            ThingModel EnvPath13 = new ThingModel("FFFFF", new List<ThingModel>() { EnvPath11, EnvPath12 });
            EnvPath11.SetParent(EnvPath13);
            EnvPath12.SetParent(EnvPath13);

            ThingModel EnvPath14 = new ThingModel("GGGGGG", new List<ThingModel>() { EnvPath10, EnvPath13 });
            EnvPath10.SetParent(EnvPath14);
            EnvPath13.SetParent(EnvPath14);

            ThingModel EnvPath15 = new ThingModel("Root", new List<ThingModel>() { EnvPath7, EnvPath14 });
            EnvPath7.SetParent(EnvPath15);
            EnvPath14.SetParent(EnvPath15);
            return EnvPath15;
        }
    }
}

Things Data Context

The Things Data Context is the class that supports the WPF application.

//ThingsDataContext.cs

using System.Collections.Generic;
using treeview.Models;

namespace treeview.ViewModels
{
    public class ThingsDataContext:ViewModelBase
    {
        private ThingsViewModel _thingsViewModel;
        private List<ThingViewModel> _thingViewModel = new List<ThingViewModel>();
        public List<ThingViewModel> ThingViewModels => _thingViewModel;

        public ThingsDataContext(ThingsViewModel thingsViewModel)
        {
            _thingsViewModel = thingsViewModel;
            ThingModel thingModel = thingsViewModel.GetAllThings();
            _thingViewModel.Add(new ThingViewModel(thingModel, true));
        }
    }
}

Commands

The Commands provide the actionable code for the user interface and if the user interface element i.e. button can be actioned.

Command Base

The Command Base is an abstract class that provides the Event Handler and Methods that any Command Class must Override or Inherit.

//CommandBase.cs

using System;
using System.Windows.Input;

namespace treeview.Commands
{
    public abstract class CommandBase : ICommand
    {
        public event EventHandler CanExecuteChanged;

        public virtual bool CanExecute(object parameter)
        {
            return true;
        }

        public abstract void Execute(object parameter);

        protected void OnCanExecutedChanged()
        {
            CanExecuteChanged?.Invoke(this, new EventArgs());
        }
    }
}

Checkbox Command

The Checkbox Command describes what should happen when any of the check boxes in the tree view are checked or unchecked.

//CheckBoxCommand.cs

using System.Windows.Controls;
using treeview.ViewModels;

namespace treeview.Commands
{
    public class CheckboxCommand : CommandBase
    {
        ThingViewModel _thingViewModel;
        public CheckboxCommand(ThingViewModel thingViewModel)
        {
            _thingViewModel = thingViewModel;
        }
        public override void Execute(object parameter)
        {
            if (parameter is CheckBox)
            {
                CheckBox checkBox = (CheckBox)parameter;
                if(checkBox.DataContext is ThingViewModel)
                {
                    ThingViewModel thingsViewModel = (ThingViewModel)checkBox.DataContext;

                    if (checkBox.IsChecked == true)
                    {
                        thingsViewModel.CheckAllChildren();
                        thingsViewModel.CheckAllParents();
                    }
                    else
                    {
                        thingsViewModel.UnCheckAllChildren();
                        thingsViewModel.UnCheckAllParents();
                    }
                }
            }
        }
    }
}

Main UI

The Main UI is going to be just a tree view nothing to exciting.

Main Window.xaml.cs

Within the Main Window we will define the data context normally in MVVM this would be loosely defined either in the startup or in the main window as a xaml, view model pairing.

//MainWindow.xaml.cs

using System.Windows;
using treeview.ViewModels;

namespace treeview
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = new ThingsDataContext(new ThingsViewModel());
        }
    }
}

Main Window.xaml

The xaml contains the treeview and hierarchical data template that allows us to traverse down the data structure.

//MainWindow.xaml

<Window x:Class="treeview.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:treeview"
        xmlns:interface="clr-namespace:treeview.ViewModels"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">

    <Grid>       
        <TreeView x:Name="MainTreeView" HorizontalAlignment="Stretch" Margin="10" VerticalAlignment="Stretch" ItemsSource="{Binding ThingViewModels}">
            <TreeView.ItemTemplate>
                <HierarchicalDataTemplate DataType="{x:Type interface:IThingViewModel}" ItemsSource="{Binding ViewModelChildren}">
                    <StackPanel Orientation="Horizontal">
                        <CheckBox IsChecked="{Binding IsChecked,Mode=TwoWay}" 
                                  Command="{Binding CheckboxCommand}" 
                                  CommandParameter="{Binding RelativeSource={RelativeSource Self}}"
                                  Margin="0 0 10 0"/>
                        <TextBlock Text="{Binding Name}"/>
                        <TextBlock Text="{Binding State}" Margin="20 0 0 0"/>
                    </StackPanel>
                </HierarchicalDataTemplate>
            </TreeView.ItemTemplate>
        </TreeView>
    </Grid>
</Window>

Final Result

The final UI contains the tree view which has triple state check boxes within and based on the check status of the leaf nodes will display either and empty box, a checked box or a filled box.

Final Result