Introduction
Formatting WPF DataGrid
content depending on business logic data is way too difficult, especially since MSDN is not telling you anything about it. I have spent weeks to figure out how to get the binding right. Let me show you how it is done to save you time and endless searches on the Internet.
WPF DataGrid Structure
The container hierarchy of a DataGrid
looks like this:
DataGrid
DataGridRows
DataGridCell
TextBlock
A DataGrid
contains DataGridRow
s which contain DataGridCell
s which contain exactly one TextBlock
, if it is a TextColumn
and in read mode (editing mode uses a TextBox
). Of course, the visual tree is a bit more complicated:
Note that DataGridColumn
is not part of the visual tree. Whatever is defined in DataGridColumn
will be applied to all cells of that column.
WPF Binding Basics
The binding gets assigned to a FrameworkElement
property, which constitutes the target of the binding.
WPF needs two source information to make a binding work:
- Source: Which object provides the information
- Path: Which property of the source should be used
Usually, the Source
gets inherited from the DataContext
of a parent container, often the Window itself. But DataGrid
's DataContext
cannot be used for the binding of rows and cells, because each row needs to bind to a different business logic object.
DataGridColumn
specifies the binding for the value to be displayed in the cell with the DataGridColumn.Binding
property. The DataGrid
creates during runtime a binding for every TextBlock.Text
. Unfortunately, the DataGrid
does not support binding for any other property of TextBlock
. If you try to setup a style for the TextBlock
yourself, the binding will most likely fail, because it wouldn't know which business object from the ItemsSource
to use.
Business Data Used
The business data example is based on some stock taking figures. A stock item looks like this:
public class StockItem {
public string Name { get; set; }
public int Quantity { get; set; }
public bool IsObsolete { get; set; }
}
The sample data:
Name | Quantity | IsObsolete |
Many items | 100 | false |
Enough items | 10 | false |
Shortage item | 1 | false |
Item with error | -1 | false |
Obsolete item | 200 | true |
Connecting a DataGrid with Business Data
Even connecting a DataGrid
with the business data is not trivial. Basically, a CollectionViewSource
is used to connect the DataGrid
with the business data:
The CollectionViewSource
does the actual data navigation, sorting, filtering, etc.
<Window.Resources>
<CollectionViewSource x:Key="ItemCollectionViewSource" CollectionViewType="ListCollectionView"/>
</Window.Resources>
<DataGrid
DataContext="{StaticResource ItemCollectionViewSource}"
ItemsSource="{Binding}"
AutoGenerateColumns="False"
CanUserAddRows="False">
var itemList = new List<stockitem>();
itemList.Add(new StockItem {Name= "Many items", Quantity=100, IsObsolete=false});
itemList.Add(new StockItem {Name= "Enough items", Quantity=10, IsObsolete=false});
...
CollectionViewSource itemCollectionViewSource;
itemCollectionViewSource = (CollectionViewSource)(FindResource("ItemCollectionViewSource"));
itemCollectionViewSource.Source = itemList;
- Define a
CollectionViewSource
in Windows.Resource
- The gotcha here is that you must set the
CollectionViewType
. If you don't, the GridView
will use BindingListCollectionView
, which does not support sorting. Of course, MSDN does not explain this anywhere. - Set the
DataContext
of the DataGrid
to the CollectionViewSource
. - In the code behind, find the
CollectionViewSource
and assign your business data to the Source
property
In this article, data gets only read. If the user should be able to edit the data, use an ObservableCollection
.
DataGrid Formatting
Formatting a Column
Formatting a whole column is easy. Just set the property, like Fontweight
directly in the DataGridColumn
:
<DataGridTextColumn Binding="{Binding Path=Name}" Header="Name" FontWeight="Bold"/>
The binding here is not involved with the formatting, but specifies the content of the cell (i.e., Text
property of TextBlock
).
Formatting Complete Rows
Formatting the rows is special, because there will be many rows. The DataGrid
offers for this purpose the RowStyle
property. This style will be applied to every DataGridRow
.
<datagrid.rowstyle>
<style targettype="DataGridRow">
<Setter Property="Background" Value="{Binding RelativeSource={RelativeSource Self},
Path=Item.Quantity, Converter={StaticResource QuantityToBackgroundConverter}}"/>
</style>
</datagrid.rowstyle>
The DatGridRow
has an Item
property, which contains the business logic object for that row. The binding for the DataRow
must therefore bind to itself ! The path is a bit surprising, because Item
is of type Object
and doesn't know any business data properties. But the WPF binding applies a bit of magic and finds the Quantity
property of StockItem
anyway.
In this example, the background of a row depends on the value of the Quantity
property of the business object. If there are many items in stock, the background should be white, if only few are left, the background should be grey. The QuantityToBackgroundConverter
performs the necessary calculation:
class QuantityToBackgroundConverter: IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
if (value is int) {
int quantity = (int)value;
if (quantity>=100) return Brushes.White;
if (quantity>=10) return Brushes.WhiteSmoke;
if (quantity>=0) return Brushes.LightGray;
return Brushes.White;
}
return Brushes.Yellow;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
Note
- Only certain properties can be set in a
DataGridRow
, like Background
color. Other properties, like Fonts
, have to be set in DataGridCell
, i.e., TextBlock
. - The
DataGridCell
, TextBlock
, etc. get painted over DataGridRow
. If both set a different Background
, the DataGridRowBackground
will be hidden.
Formatting a Cell Based on the Displayed Value
Formatting just a cell instead of the whole row is a challenge. In a text column, the cell has a TextBlock
which needs to be styled. To create a Style
for TextBlock
s is easy, but how can the TextBlock
property be bound to the proper business object? The DataGrid
is binding already the Text
property of the TextBlock
. If the styling depends only on the cell value, we can simply use a self binding to this Text
property.
Example: In our stock grid, the Quantity
should always be greater than or equal to zero. If a quantity is negative, it is an error and should be displayed in red:
<Setter Property="Foreground"
Value="{Binding
RelativeSource={RelativeSource Self},
Path=Text,
Converter={StaticResource QuantityToForegroundConverter}}" />
Formatting a Cell Based on Business Logic Data
The most complex case is if the cell format does not depend on the cell value, but some other business data. In our example, the quantity of an item should be displayed as strike through if it is obsolete. To achieve this, the TextDecorations
property needs to be linked to the business object of that row. Meaning the TextBlock
has to find the parent DataGridRow
. Luckily, binding to a parent visual object can be done with a relative source:
<Window.Resources>
<Style x:Key="QuantityStyle" TargetType="TextBlock">
...
<Setter Property="TextDecorations"
Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGridRow}},
Path =Item.IsObsolete,
Converter={StaticResource IsObsoleteToTextDecorationsConverter}}" />
</Style>
</Window.Resources>
...
<DataGrid ...>
...
<DataGrid.Columns>
...
<DataGridTextColumn Binding="{Binding Path=Quantity}" Header="Quantity"
ElementStyle="{StaticResource QuantityStyle}"/>
</DataGrid.Columns>
</DataGrid>
ElementStyle versus CellStyle
DataGridTextColumn
inherits from DataGridBoundColumn
, which inherits from DataGridColumn
.
From DataGridBoundColumn
, it inherits the property ElementStyle
. This style gets applied to the TextBlock
control.
From DataGridColumn
, it inherits the property Cell
. This style gets applied to the DataGridCell
control.
Note in the visual tree above that the DataGridCell
contains a TextBlock
. DataGridCell
gets painted first, then the TextBlock
over it.
Because TextBlock
and DataGridCell
inherit from Control
, both have properties for background, border, font, foreground, padding and content alignment. Meaning one can set the Background
colour in ElementStyle
or CellStyle
.
When should which be used? We use here ElementStyle
, because it supports also the setting of TextBlock
specific properties like TextDecorations
. For certain properties like Background
, CellStyle
should be used, because it covers all of the cell, while ElementStyle
sets the background of only the textblock, which covers only part of the cell.
It is also possible to set both styles. If they set the same property with different values, ElementStyle
wins.
Further Reading
If you read that far, you are truly interested in the WPF DataGrid. In that case, I would like to recommend to you another article I wrote about WPF DataGrids:
WPF DataGrid: Solving Sorting, ScrollIntoView, Refresh and Focus Problems