Table of Contents
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.
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.