WPF Treeview: Styling and Template Binding using MVVM






4.40/5 (11 votes)
Describes how to change appearance of Treeview using custom style
Introduction
In the development world, sometimes we have a requirement to change the appearance of the control. In this article, we walk through how to change appearance of Treeview
control using Templates and Styles.
We will also focus on MVVM (Model-View-ViewModel) pattern in this article.
Background
First let's look at MVVM.
MVVM (Model-View-ViewModel)
The Model View ViewModel (MVVM) is an architectural pattern used in software engineering that originated from Microsoft which is specialized in the Presentation Model design pattern. The Model-View-ViewModel (MVVM) pattern provides a flexible way to build WPF/Silverlight applications that promotes code re-use, simplifies maintenance and supports testing.
Model/View/ViewModel also relies on one more thing: a general mechanism for data binding.
View
View in MVVM is defined in XAML. It contains visual controls and rich UI using styles and templates and storyboard. View is used for Data binding and displaying data.
ViewModel
Viewmodel is the mediator between view and viewmodel. It has properties, commands and propertychanged notification logic to execute when any property raise or changed. All business logic related to view is written in viewmodel.
Model
Model is like a Entity. It defines properties to be exposed.
TREEVIEW
Treeview
is used to display data in a hierarchical series with parent child relationship. One parent node has multiple child nodes.
WPF has HierarchicalDataTemplate
to bind treeviewitem
in hierarchical data by setting ItemsSource
property. You can change the style of treeview
using custom template style.
Using the Code
This example is for a Company that has many departments and each department has employees.
We wish to bind departments in parent node and employees for each department in child nodes in Treeview
.
Step 1: Create Models for Department and Employee
public class Employee
{
public int EmployeeID { get; set; }
public string EmployeeName { get; set; }
}
public class Department
{
public int DepartmentID { get; set; }
public string DepartmentName { get; set; }
public List<Employee> EmployeeList { get; set; }
}
Step 2: Create One Class for NotifyProperty Change
ViewModelBase.cs
public abstract class ViewModelBase:INotifyPropertyChanged
{
#region Constructor
protected ViewModelBase()
{
}
#endregion
#region Events
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region Event Handlers
/// <summary>
/// Get name of property
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="e"></param>
/// <returns></returns>
public static string GetPropertyName<T>(Expression<Func<T>> e)
{
var member = (MemberExpression)e.Body;
return member.Member.Name;
}
/// <summary>
/// Raise when property value propertychanged or override propertychage
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="propertyExpression"></param>
protected virtual void RaisePropertyChanged<T>
(Expression<Func<T>> propertyExpression)
{
RaisePropertyChanged(GetPropertyName(propertyExpression));
}
/// <summary>
/// Raise when property value propertychanged
/// </summary>
/// <param name="propertyName"></param>
protected void RaisePropertyChanged(String propertyName)
{
PropertyChangedEventHandler temp = PropertyChanged;
if (temp != null)
{
temp(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
If we want to notify viewmodels property change, then we must inherit this class in every viewmodel, and override RaisePropertyChanged
method to notify property change.
Step 3: Create viewmodels for Each Model
EmployeeViewModel.cs
public class EmployeeViewModel : ViewModelBase
{
public Employee Employee { get; protected set; }
public int EmployeeID
{
get { return Employee.EmployeeID; }
set
{
if (Employee.EmployeeID != value)
{
Employee.EmployeeID = value;
RaisePropertyChanged(() => EmployeeID);
}
}
}
public string EmployeeName
{
get { return Employee.EmployeeName; }
set
{
if (Employee.EmployeeName != value)
{
Employee.EmployeeName = value;
RaisePropertyChanged(() => EmployeeName);
}
}
}
private bool _isChecked;
public bool IsChecked
{
get { return _isChecked; }
set
{
if (_isChecked != value)
{
_isChecked = value;
RaisePropertyChanged(() => IsChecked);
}
}
}
public EmployeeViewModel(Employee employee)
{
this.Employee = employee;
IsChecked = false;
}
}
This viewmodel holds employee model properties and one more property is IsChecked
.
IsChecked
property is used to check/uncheck child tree nodes and holds value of check/uncheck state for current employee of selected department.
In this viewmodel
, I am binding Employee
model properties because I have to set logic when property value is changed.
DepartmentViewModel.cs
public class DepartmentViewModel:ViewModelBase
{
public Department Department { get; protected set; }
private ObservableCollection<EmployeeViewModel> _employeeCollection;
public ObservableCollection<EmployeeViewModel> EmployeeCollection
{
get { return _employeeCollection; }
set {
if (_employeeCollection != value)
{
_employeeCollection = value;
RaisePropertyChanged(() => EmployeeCollection);
}
}
}
public int DepartmentID
{
get { return Department.DepartmentID; }
set {
if (Department.DepartmentID != value)
{
Department.DepartmentID = value;
RaisePropertyChanged(() => DepartmentID);
}
}
}
public string DepartmentName
{
get { return Department.DepartmentName; }
set {
if (Department.DepartmentName != value)
{
Department.DepartmentName = value;
RaisePropertyChanged(() => DepartmentName);
}
}
}
private bool _isChecked;
public bool IsChecked
{
get { return _isChecked; }
set
{
if (_isChecked != value)
{
_isChecked = value;
RaisePropertyChanged(() => IsChecked);
OnCheckChanged();
}
}
}
private void OnCheckChanged()
{
foreach (EmployeeViewModel employeeViewModel in EmployeeCollection)
{
employeeViewModel.IsChecked = IsChecked;
}
}
public DepartmentViewModel(Department department)
{
this.Department = department;
EmployeeCollection = new ObservableCollection<EmployeeViewModel>();
foreach (Employee employee in Department.EmployeeList)
{
EmployeeCollection.Add(new EmployeeViewModel(employee));
}
}
}
DepartmentViewModel
consists of Department Model Properties and Collections of EmployeeViewmodel
.
Why I have created property ObservableCollection<EmployeeViewModel> EmployeeCollection
is because each department has a list of employees so it should contain a list of employees.
ObservableCollection
provides dynamic data collections and it also provides notification when any items are added/deleted/updated.
IsChecked
property is used to check/uncheck department.
OnCheckChanged()
method is used to check/uncheck all employees when related department is check/uncheck.
Step 4: Create viewmodel for UI Page
CompanyDetailViewModel.cs
public class CompanyDetailViewModel : ViewModelBase
{
private ObservableCollection<DepartmentViewModel> _departmentCollection;
public ObservableCollection<DepartmentViewModel> DepartmentCollection
{
get { return _departmentCollection; }
set
{
if (_departmentCollection != value)
{
_departmentCollection = value;
RaisePropertyChanged(() => DepartmentCollection);
}
}
}
public CompanyDetailViewModel()
{
DepartmentCollection = new ObservableCollection<DepartmentViewModel>();
List<Department> departmentList = GetDepartmentList();
foreach (Department department in departmentList)
{
DepartmentCollection.Add(new DepartmentViewModel(department));
}
}
#region Methods
List<Employee> GetEmployeeList()
{
List<Employee> employeeList = new List<Employee>();
employeeList.Add(new Employee() { EmployeeID = 1, EmployeeName = "Hiren" });
employeeList.Add(new Employee() { EmployeeID = 2, EmployeeName = "Imran" });
employeeList.Add(new Employee() { EmployeeID = 3, EmployeeName = "Shivpal" });
employeeList.Add(new Employee() { EmployeeID = 4, EmployeeName = "Prabhat" });
employeeList.Add(new Employee() { EmployeeID = 5, EmployeeName = "Sandip" });
employeeList.Add(new Employee() { EmployeeID = 6, EmployeeName = "Chetan" });
employeeList.Add(new Employee() { EmployeeID = 7, EmployeeName = "Jayesh" });
employeeList.Add(new Employee() { EmployeeID = 8, EmployeeName = "Bhavik" });
employeeList.Add(new Employee() { EmployeeID = 9, EmployeeName = "Amit" });
employeeList.Add(new Employee() { EmployeeID = 10, EmployeeName = "Brijesh" });
return employeeList;
}
List<Department> GetDepartmentList()
{
List<Employee> employeeList = GetEmployeeList();
List<Department> departmentList = new List<Department>();
departmentList.Add(new Department() { DepartmentID = 1,
DepartmentName = "Mocrosoft.Net",
EmployeeList = employeeList.Take(3).ToList() });
departmentList.Add(new Department() { DepartmentID = 2,
DepartmentName = "Open Source",
EmployeeList = employeeList.Skip(3).Take(3).ToList() });
departmentList.Add(new Department() { DepartmentID = 3,
DepartmentName = "Other",
EmployeeList = employeeList.Skip(6).Take(4).ToList() });
return departmentList;
}
#endregion
This viewmodel is for CompanyDetailView.xaml page declared ObservableCollection<DepartmentViewModel> DepartmentCollection
property to hold collection of departments to bind in treeviewitem
s.
I created two methods for getting a list of Department
s and List of Employee
s and bind to each viewmodel properties.
Step 5: Create XAML Page to Display treeview
CompanyDetailView.xaml
<Grid Grid.Row="1"
MaxHeight="250">
<TreeView ScrollViewer.VerticalScrollBarVisibility="Auto"
BorderThickness="0"
Background="#FFF"
ItemContainerStyle="{DynamicResource TreeViewItemStyle}"
ItemsSource="{Binding DepartmentCollection}"
ItemTemplate="{DynamicResource DepartmentTemplate}" />
</Grid>
Put Treeview
control in page and setting ItemsSource
to DepartmentCollection
property of CompanyDetailViewModel
.
Set ItemContainerStyle
to TreeViewItemStyle
created in resourcedictionary
.
Set ItemTemplate
to DepartmentTemplate
, i.e., template created in resourcedictionary
.
Step 6: Create TreeViewItemStyle Style
<Style x:Key="ExpandCollapseToggleStyle"
TargetType="{x:Type ToggleButton}">
<Setter Property="Focusable"
Value="False" />
<Setter Property="Width"
Value="17" />
<Setter Property="Height"
Value="17" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Border Width="17"
Height="17"
Background="Transparent">
<Border Width="17"
Height="17"
SnapsToDevicePixels="true"
Background="{DynamicResource
{x:Static SystemColors.WindowBrushKey}}"
BorderBrush="{DynamicResource
{x:Static SystemColors.GrayTextBrushKey}}"
BorderThickness="1">
<Grid>
<Rectangle>
<Rectangle.Fill>
<LinearGradientBrush EndPoint="0,1"
StartPoint="0,0">
<GradientStop Color="#7FD4FF"
Offset="0" />
<GradientStop Color="#00AAFF"
Offset="1" />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Line Stroke="#112844"
x:Name="ExpandImg"
StrokeThickness="1"
X1="8"
Y1="2"
X2="8"
Y2="14" />
<Line Stroke="#112844"
StrokeThickness="1"
X1="2"
Y1="8"
X2="14"
Y2="8" />
</Grid>
</Border>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked"
Value="True">
<Setter Property="Visibility"
TargetName="ExpandImg"
Value="Collapsed" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
In the above code, I have created style for toggle button (Expand/Collapsed) in treeview
.
<Rectangle>
control is used to create a square.
<Line>
control is used to create + (Collapsed) and - (Expand) symbol.
<Trigger>
is for change state of toggle button when expand/collapsed.
<Style x:Key="TreeViewItemStyle"
TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded"
Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="Background"
Value="Transparent" />
<Setter Property="HorizontalContentAlignment"
Value="{Binding HorizontalContentAlignment,
RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" />
<Setter Property="VerticalContentAlignment"
Value="{Binding VerticalContentAlignment,
RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" />
<Setter Property="Padding"
Value="1,0,0,0" />
<Setter Property="Foreground"
Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" />
<Setter Property="FocusVisualStyle"
Value="{StaticResource TreeViewItemFocusVisual}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TreeViewItem}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="19"
Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<ToggleButton x:Name="Expander"
ClickMode="Press"
IsChecked="{Binding IsExpanded,
RelativeSource={RelativeSource TemplatedParent}}"
Style="{StaticResource ExpandCollapseToggleStyle}" />
<Border x:Name="Bd"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
Grid.Column="1"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="true">
<ContentPresenter x:Name="PART_Header"
ContentSource="Header"
VerticalAlignment=
"{TemplateBinding VerticalContentAlignment}"
HorizontalAlignment=
"{TemplateBinding HorizontalContentAlignment}"
SnapsToDevicePixels=
"{TemplateBinding SnapsToDevicePixels}" />
</Border>
<ItemsPresenter x:Name="ItemsHost"
Grid.ColumnSpan="2"
Grid.Column="1"
Grid.Row="1" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="VirtualizingStackPanel.IsVirtualizing"
Value="true">
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
Above is the style for treeviewitem
to set Background
color property, setting controlTemplate
, Padding
, Alignment
, etc. properties.
Step 7: Create Templates for Display TreeviewItes
<DataTemplate x:Key="EmployeeDateTemplate">
<StackPanel Orientation="Horizontal">
<CheckBox Focusable="False"
IsChecked="{Binding IsChecked,Mode=TwoWay}"
VerticalAlignment="Center" Margin="5" />
<TextBlock Text="{Binding EmployeeName}"
VerticalAlignment="Center" Margin="5" />
</StackPanel>
</DataTemplate>
<HierarchicalDataTemplate x:Key="DepartmentTemplate"
ItemsSource="{Binding EmployeeCollection}"
ItemTemplate="{StaticResource EmployeeDateTemplate}">
<StackPanel Orientation="Horizontal">
<CheckBox Focusable="False" Margin="5"
IsChecked="{Binding IsChecked,Mode=TwoWay}"
VerticalAlignment="Center" />
<TextBlock Text="{Binding DepartmentName}"
Margin="5 5 10 10" />
</StackPanel>
HierarchicalDataTemplate
Template binds data in treeview
items as parent nodes and holds child data template. Its ItemsSource
property holds data of Collection of Employee
related to Department
.
ItemTemplate
property has template for how to display employee
data in treeview
items. It is DataTemplate
for Employee
.
Step 8: Setting DataContext to View
this.DataContext = new CompanyDetailViewModel();
The above line is written in code behind file of MainWindow.xaml to set datacontent
of CompanyDetailViewModel
which contains Company Data.
At the end, put CompanyDetailView.xaml control in MainWindow.xaml and run the code. You will see the below output:
History
- 8th August, 2011: Initial post