WPF ListBox data templating/styling
Introduction
Each control, TextBlock, TextBox, ListBox etc. have their own default template associated with it. Using styles, controls can be modify from their default template associated. WPF enables you to change the look and feel of the controls and this can be achieved by using templates. This article with both C# and Visual Basic projects will show how to utilize templating and styling to display a collection of items in a ListBox were each task displayed on a row with description of a property and the value for a property coupled with a ContentControl to display three out of four properties which may be edited then on tabbing out of a TextBox display the changed value in the current selected item in the ListBox.
Citation With Windows Presentation Foundation (WPF), you can customize an existing control's visual structure and behavior with your own reusable template. Templates can be applied globally to your application, windows and pages, or directly to controls. Most scenarios that require you to create a new control can be covered by instead creating a new template for an existing control. |
Application wide styling There are two project provided, one Visual Basic, one C#. In both projects the main window has a StackPanel within a Grid and gets styling from App.xaml for C#, Application.xaml for Visual Basic. TextBox controls have application wide styling in the same file. |
Project overview
Provide a display of task in a ListBox which may be edited in TextBox controls followed by updating the current row in the ListBox. As the focus is on data templating and styling data presented is mocked data while in reality the data may come from a comma delimited file or a database, no matter were the data comes from does not affect styling or binding of data.
Data templates dictate how bound data is mapped to one or more control. A data template can be used in two places:
- As value of ContentTemplate property for a ContentControl (e.g. a Label)
- As value of ItemTemplate property for an ItemsControl (e.g. a ListBox)
Data classes representing a Task
A Task is comprised of a name, description, task type and priority.
For Task type an enum is used.
C#
public enum TaskType
{
Home,
Work
}
VB.NET
Public Enum TaskType
Home
Work
End Enum
The following class represents a Task which implementings INotifyPropertyChanged so that when a property is changed in TextBox controls within the ContentControl the ListBox source reflects changes through an ObservableCollection<TaskItem>.
C#
using System.ComponentModel;
using System.Runtime.CompilerServices;
using JetBrains.Annotations;
namespace WpfAppExample1.Classes
{
public class TaskItem : INotifyPropertyChanged
{
private string _description;
private string _taskName;
private int _priority;
public string TaskName
{
get => _taskName;
set
{
if (value == _taskName) return;
_taskName = value;
OnPropertyChanged();
}
}
public string Description
{
get => _description;
set
{
if (value == _description) return;
_description = value;
OnPropertyChanged();
}
}
public TaskType TaskType { get; set; }
public int Priority
{
get => _priority;
set
{
if (value == _priority) return;
_priority = value;
OnPropertyChanged();
}
}
public override string ToString() => TaskName;
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
VB.NET
Imports System.ComponentModel
Imports System.Runtime.CompilerServices
Imports JetBrains.Annotations
Namespace Classes
Public Class TaskItem
Implements INotifyPropertyChanged
Private _description As String
Private _taskName As String
Private _priority As Integer
Public Property TaskName() As String
Get
Return _taskName
End Get
Set(ByVal value As String)
If value = _taskName Then
Return
End If
_taskName = value
OnPropertyChanged()
End Set
End Property
Public Property Description() As String
Get
Return _description
End Get
Set(ByVal value As String)
If value = _description Then
Return
End If
_description = value
OnPropertyChanged()
End Set
End Property
Public Property TaskType() As TaskType
Public Property Priority() As Integer
Get
Return _priority
End Get
Set(ByVal value As Integer)
If value = _priority Then
Return
End If
_priority = value
OnPropertyChanged()
End Set
End Property
Public Overrides Function ToString() As String
Return TaskName
End Function
Public Event PropertyChanged As PropertyChangedEventHandler _
Implements INotifyPropertyChanged.PropertyChanged
<NotifyPropertyChangedInvocator>
Protected Overridable Sub OnPropertyChanged(<CallerMemberName>
Optional ByVal propertyName As String = Nothing)
PropertyChangedEvent?.Invoke(Me, New PropertyChangedEventArgs(propertyName))
End Sub
End Class
End Namespace
Mocked data class using ObservableCollection<TaskItem> which is instantiated as a public variable in the window containing the ListBox.
C#
using System.ComponentModel;
using System.Runtime.CompilerServices;
using JetBrains.Annotations;
namespace WpfAppExample1.Classes
{
public class TaskItem : INotifyPropertyChanged
{
private string _description;
private string _taskName;
private int _priority;
public string TaskName
{
get => _taskName;
set
{
if (value == _taskName) return;
_taskName = value;
OnPropertyChanged();
}
}
public string Description
{
get => _description;
set
{
if (value == _description) return;
_description = value;
OnPropertyChanged();
}
}
public TaskType TaskType { get; set; }
public int Priority
{
get => _priority;
set
{
if (value == _priority) return;
_priority = value;
OnPropertyChanged();
}
}
public override string ToString() => TaskName;
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
VB.NET
Imports System.Collections.ObjectModel
Namespace Classes
Public Class Tasks
Public Function List() As ObservableCollection(Of TaskItem)
Return New ObservableCollection(Of TaskItem)() From {
New TaskItem() With {
.Priority = 2,
.TaskType = TaskType.Work,
.TaskName = "Unit test data operations",
.Description = "Delegate to junior developer"
},
New TaskItem() With {
.Priority = 1,
.TaskType = TaskType.Work,
.TaskName = "Prototype dashboard",
.Description = "Put together dashboard prototype"
},
New TaskItem() With {
.Priority = 1,
.TaskType = TaskType.Home,
.TaskName = "Cook dinner",
.Description = "Ah, get a pizza"
},
New TaskItem() With {
.Priority = 3,
.TaskType = TaskType.Work,
.TaskName = "Single signon discussion",
.Description = "Discuss options"
}
}
End Function
End Class
End Namespace
In the main window constructor mocked data is assigned to an ObservableCollection.
C#
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using WpfAppExample1.Classes;
namespace WpfAppExample1
{
/// <summary>
/// Interaction logic for ListWindow1.xaml
/// </summary>
public partial class ListWindow1 : Window
{
public ObservableCollection<TaskItem> TaskItemsList { get; set; }
public ListWindow1()
{
InitializeComponent();
var taskOperations = new Tasks();
TaskItemsList = taskOperations.List();
DataContext = this;
}
/// <summary>
/// Ensure only int values are entered.
/// A robust alternate is using Data Annotations
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void NumberValidation(object sender, TextCompositionEventArgs e)
{
var regex = new Regex("[^0-9]+");
e.Handled = regex.IsMatch(e.Text);
}
}
}
VB.NET
Imports System.Collections.ObjectModel
Imports System.Text.RegularExpressions
Imports WpfAppExample2.Classes
Class MainWindow
Public Property TaskItemsList() As ObservableCollection(Of TaskItem)
Public Sub New()
InitializeComponent()
Dim taskOperations = New Tasks()
TaskItemsList = taskOperations.List()
DataContext = Me
End Sub
''' <summary>
''' Ensure only int values are entered.
''' A robust alternate is using Data Annotations
''' </summary>
''' <param name="sender"></param>
''' <param name="e"></param>
Private Sub NumberValidation(sender As Object, e As TextCompositionEventArgs)
Dim regex = New Regex("[^0-9]+")
e.Handled = regex.IsMatch(e.Text)
End Sub
End Class
XAML Code
The following data template defines grid rows/columns to present data where the type for each row is of type TaskItem (C#) (VB).
<DataTemplate x:Key="ListBoxTaskTemplate" DataType="classes:TaskItem">
<Border Name="border" BorderBrush="LightGray" BorderThickness="1" Padding="2" Margin="2">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Task Name:"/>
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Path=TaskName}" />
<TextBlock Grid.Row="1" Grid.Column="0" Text="Description:"/>
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding Path=Description}"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="Priority:"/>
<TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding Path=Priority}"/>
</Grid>
</Border>
</DataTemplate>
In the ListBox, IsSynchronizedWithCurrentItem = true will keep the current SelectedItem synchronized with the current item in the Items property, in this case a TaskItem.
<ListBox x:Name="TaskListBox" HorizontalAlignment="Left" Height="262" Margin="29,14,0,0"
VerticalAlignment="Top" Width="417" HorizontalContentAlignment="Stretch"
IsSynchronizedWithCurrentItem="True"
ItemTemplate="{StaticResource ListBoxTaskTemplate}"
ItemsSource="{Binding TaskItemsList, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
Then the following sets the data template for the ListBox to use.
ItemTemplate="{StaticResource ListBoxTaskTemplate}"
Followed by the actual binding to the ObservableCollection from code behind. Note Mode=TwoWay as this permits editing from TextBox controls in tangent with UpdateSourceTrigger=PropertyChanged which ties into INotifiyPropertyChanged Interface implemented in the class Taskitem.
ItemsSource="{Binding TaskItemsList, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
The following style trigger will paint each item in the ListBox based off the TaskType. An alternative is to not have both task types in the ListBox, instead filter on the list based off the TaskType.
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Style.Triggers>
<DataTrigger Binding="{Binding TaskType}" Value="Home">
<Setter Property="Background" Value="Yellow" />
</DataTrigger>
<DataTrigger Binding="{Binding TaskType}" Value="Work">
<Setter Property="Background" Value="White" />
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
The following constructs a data template for TextBlock and TextBox controls bound to the currently selected TaskItem in the ListBox. For the property Priority WPF automatically handles non-numeric values, leave the TextBox and the border will turn red, this does not tell much to the uneducated user so in PreviewTextInput regular expressions are used to allow only numerics.
<DataTemplate x:Key="CurrentDetailsTemplate" DataType="classes:TaskItem">
<Border
Background="#DCE7F5" BorderBrush="Silver"
CornerRadius="4,4,4,4" Width="Auto" Height="100" Margin="32,20,30,20" BorderThickness=".85" Padding="8">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="28*"/>
<RowDefinition Height="28*"/>
<RowDefinition Height="28*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="61*"/>
<ColumnDefinition Width="130*"/>
</Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Right" Grid.Row="0" Grid.Column="0" Text="Name:" Padding="0,0,10,0"/>
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding TaskName}" Margin="0,0,0,4"/>
<TextBlock HorizontalAlignment="Right" Grid.Row="1" Grid.Column="0" Text="Description:" Padding="0,0,10,0"/>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Description}" Margin="0,0,0,3"/>
<TextBlock HorizontalAlignment="Right" Grid.Row="2" Grid.Column="0" Text="Priority:" Padding="0,0,10,0"/>
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding Priority}" Margin="0,3,0,0" PreviewTextInput="NumberValidation" />
</Grid>
</Border>
</DataTemplate>
This is following by the following to paint the controls above.
<ContentControl Content="{Binding TaskItemsList}" ContentTemplate="{StaticResource CurrentDetailsTemplate}"/>
Polishing up
Presenting a window should have an active control, in this case the ListBox should be focused and ready to use. The following markup provides this along with code behind.
Code behind
C#
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace WpfAppExample1.Classes
{
/// <summary>
/// attached behavior for selecting first control in a window
/// </summary>
public static class FocusBehavior
{
public static readonly DependencyProperty GiveInitialFocusProperty =
DependencyProperty.RegisterAttached("GiveInitialFocus",typeof(bool),typeof(FocusBehavior),
new PropertyMetadata(false, OnFocusFirstPropertyChanged));
public static bool GetGiveInitialFocus(Control control) => (bool)control.GetValue(GiveInitialFocusProperty);
public static void SetGiveInitialFocus(Control control, bool value) => control.SetValue(GiveInitialFocusProperty, value);
private static void OnFocusFirstPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
if (!(sender is Control control) || !(args.NewValue is bool))
{
return;
}
if ((bool)args.NewValue)
{
control.Loaded += OnControlLoaded;
}
else
{
control.Loaded -= OnControlLoaded;
}
}
private static void OnControlLoaded(object sender, RoutedEventArgs e) =>
((Control)sender).MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
}
}
VB.NET
Namespace Classes
''' <summary>
''' attached behavior for selecting first control in a window
''' </summary>
Public NotInheritable Class FocusBehavior
Private Sub New()
End Sub
Public Shared ReadOnly GiveInitialFocusProperty As DependencyProperty =
DependencyProperty.RegisterAttached("GiveInitialFocus",
GetType(Boolean), GetType(FocusBehavior),
New PropertyMetadata(False, AddressOf OnFocusFirstPropertyChanged))
Public Shared Function GetGiveInitialFocus(ByVal control As Control) As Boolean
Return DirectCast(control.GetValue(GiveInitialFocusProperty), Boolean)
End Function
Public Shared Sub SetGiveInitialFocus(ByVal control As Control, ByVal value As Boolean)
control.SetValue(GiveInitialFocusProperty, value)
End Sub
Private Shared Sub OnFocusFirstPropertyChanged(ByVal sender As DependencyObject,
ByVal args As DependencyPropertyChangedEventArgs)
Dim tempVar As Boolean = TypeOf sender Is Control
Dim control As Control = If(tempVar, CType(sender, Control), Nothing)
If Not (tempVar) OrElse Not (TypeOf args.NewValue Is Boolean) Then
Return
End If
If DirectCast(args.NewValue, Boolean) Then
AddHandler control.Loaded, AddressOf OnControlLoaded
Else
RemoveHandler control.Loaded, AddressOf OnControlLoaded
End If
End Sub
Private Shared Sub OnControlLoaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
DirectCast(sender, Control).MoveFocus(New TraversalRequest(FocusNavigationDirection.Next))
End Sub
End Class
End Namespace
Summary
Code, both xaml and code behind has been presented to template/style a ListBox in sync with TextBox controls which can be used in project which require displaying specific information with the ability to edit. Not part of the article is reading data from a data source and save back to a data source, the foundation is here to add this logic as a developer sees fit from file streams to Entity Framework Core.
See also
WPF: Tips - Bind to Current item of Collection
WPF Data, Item and Control Templates - Minimum Code, Maximum Awesomeness
Different ways to dynamically select DataTemplate for WPF ListView
Moving from WinForms to WPF
WPF Control Templates