使用绑定进行 WPF DataGrid 格式化的指南






4.94/5 (41投票s)
使用 Style 和 Binding 来控制 WPF DataGridCell 的外观
引言
根据业务逻辑数据对WPF DataGrid
内容进行格式化非常困难,尤其是因为MSDN对此几乎没有任何说明。我花了几个星期才弄清楚如何正确设置绑定。让我向您展示如何做到这一点,以节省您的时间和无休止的网络搜索。
WPF DataGrid 结构
DataGrid
的容器层次结构如下所示
DataGrid
DataGridRows
DataGridCell
TextBlock
一个 DataGrid
包含 DataGridRow
,DataGridRow
包含 DataGridCell
,如果它是 TextColumn
且处于读取模式(编辑模式使用 TextBox
),则 DataGridCell
包含一个 TextBlock
。当然,视觉树会更复杂一些。
请注意,DataGridColumn
不是视觉树的一部分。在 DataGridColumn
中定义的任何内容都将应用于该列的所有单元格。
WPF 绑定基础
绑定被分配给一个 FrameworkElement
属性,该属性构成了绑定的目标。
WPF 需要两个源信息才能使绑定工作
- Source (源):哪个对象提供信息
- Path (路径):应使用源的哪个属性
通常,Source
从父容器的 DataContext
继承,通常是窗口本身。但是,DataGrid
的 DataContext
不能用于行和单元格的绑定,因为每行都需要绑定到不同的业务逻辑对象。
DataGridColumn
使用 DataGridColumn.Binding
属性指定要在单元格中显示的绑定的值。DataGrid
在运行时为每个 TextBlock.Text
创建一个绑定。不幸的是,DataGrid
不支持绑定 TextBlock
的任何其他属性。如果您尝试自己设置 TextBlock
的样式,绑定很可能会失败,因为它不知道应使用 ItemsSource
中的哪个业务对象。
使用的业务数据
业务数据示例基于一些库存盘点数据。一个库存项看起来像这样
public class StockItem {
public string Name { get; set; }
public int Quantity { get; set; }
public bool IsObsolete { get; set; }
}
示例数据
名称 | 数量 | IsObsolete (是否已过时) |
许多商品 | 100 | false |
足够多的商品 | 10 | false |
缺货商品 | 1 | false |
有错误的项目 | -1 | false |
已过时商品 | 200 | true |
连接 DataGrid 与业务数据
即使是连接 DataGrid
与业务数据也并非易事。基本上,CollectionViewSource
用于将 DataGrid
与业务数据连接起来。
CollectionViewSource
负责实际的数据导航、排序、过滤等。
<Window.Resources>
<CollectionViewSource x:Key="ItemCollectionViewSource" CollectionViewType="ListCollectionView"/>
</Window.Resources>
<DataGrid
DataContext="{StaticResource ItemCollectionViewSource}"
ItemsSource="{Binding}"
AutoGenerateColumns="False"
CanUserAddRows="False">
//create business data
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});
...
//link business data to CollectionViewSource
CollectionViewSource itemCollectionViewSource;
itemCollectionViewSource = (CollectionViewSource)(FindResource("ItemCollectionViewSource"));
itemCollectionViewSource.Source = itemList;
- 在
Windows.Resource
中定义CollectionViewSource
- 这里要注意的是,您 **必须** 设置
CollectionViewType
。如果不行,GridView
将使用BindingListCollectionView
,它不支持排序。当然,MSDN 在任何地方都没有解释这一点。 - 将
DataGrid
的DataContext
设置为CollectionViewSource
。 - 在代码隐藏中,找到
CollectionViewSource
并将您的业务数据分配给Source
属性。
在本文中,数据仅被读取。如果用户应该能够编辑数据,请使用 ObservableCollection
。
DataGrid格式化
格式化列
格式化整列很容易。只需在 DataGridColumn
中直接设置属性,例如 Fontweight
。
<DataGridTextColumn Binding="{Binding Path=Name}" Header="Name" FontWeight="Bold"/>
这里的绑定不涉及格式化,而是指定单元格的内容(即 TextBlock
的 Text
属性)。
格式化整行
格式化行比较特殊,因为会有很多行。DataGrid
为此提供了 RowStyle
属性。此样式将应用于每个 DataGridRow
。
<datagrid.rowstyle>
<style targettype="DataGridRow">
<Setter Property="Background" Value="{Binding RelativeSource={RelativeSource Self},
Path=Item.Quantity, Converter={StaticResource QuantityToBackgroundConverter}}"/>
</style>
</datagrid.rowstyle>
DatGridRow
有一个 Item
属性,其中包含该行的业务逻辑对象。因此,DataRow
的绑定必须绑定到自身!路径有点令人惊讶,因为 Item
的类型是 Object
,并且不知道任何业务数据属性。但是 WPF 绑定会应用一些魔力,并找到 StockItem
的 Quantity
属性。
在此示例中,行的背景取决于业务对象的 Quantity
属性的值。如果库存商品很多,背景应为白色;如果仅剩少量,背景应为灰色。QuantityToBackgroundConverter
执行必要的计算。
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; //quantity should not be below 0
}
//value is not an integer. Do not throw an exception
// in the converter, but return something that is obviously wrong
return Brushes.Yellow;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
注意
- 只能在
DataGridRow
中设置某些属性,例如Background
颜色。其他属性,如Fonts
,必须在DataGridCell
中设置,即TextBlock
。 DataGridCell
、TextBlock
等在DataGridRow
上绘制。如果两者都设置了不同的Background
,则DataGridRowBackground
将被隐藏。
基于显示值格式化单元格
仅格式化单元格而不是整行是一个挑战。在文本列中,单元格有一个需要设置样式的 TextBlock
。创建 TextBlock
的 Style
很容易,但 TextBlock
属性如何绑定到正确的业务对象?DataGrid
已经在绑定 TextBlock
的 Text
属性。如果样式仅取决于单元格的值,我们可以简单地使用对该 Text
属性的自绑定。
示例:在我们的库存网格中,Quantity
应始终大于或等于零。如果数量为负,则表示错误,应显示为红色。
<Setter Property="Foreground"
Value="{Binding
RelativeSource={RelativeSource Self},
Path=Text,
Converter={StaticResource QuantityToForegroundConverter}}" />
基于业务逻辑数据格式化单元格
最复杂的情况是,如果单元格格式不取决于单元格值,而是取决于其他业务数据。在我们的示例中,如果商品已过时,其数量应显示为删除线。为实现此目的,需要将 TextDecorations
属性链接到该行的业务对象。这意味着 TextBlock
必须找到父 DataGridRow
。幸运的是,绑定到父视觉对象可以通过相对源完成。
<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 与 CellStyle
DataGridTextColumn
继承自 DataGridBoundColumn
,而 DataGridBoundColumn
又继承自 DataGridColumn
。
从 DataGridBoundColumn
,它继承了 ElementStyle
属性。此样式应用于 TextBlock
控件。
从 DataGridColumn
,它继承了 Cell
属性。此样式应用于 DataGridCell
控件。
请注意,在上面的视觉树中,DataGridCell
包含一个 TextBlock
。DataGridCell
首先被绘制,然后 TextBlock
在其之上绘制。
由于 TextBlock
和 DataGridCell
都继承自 Control
,因此它们都具有用于背景、边框、字体、前景、填充和内容对齐的属性。这意味着可以在 ElementStyle
或 CellStyle
中设置 Background
颜色。
何时使用哪一个?我们在这里使用 ElementStyle
,因为它还支持设置 TextBlock
特定的属性,例如 TextDecorations
。对于某些属性,如 Background
,应使用 CellStyle
,因为它覆盖了整个单元格,而 ElementStyle
只设置了 TextBlock
的背景,只覆盖了单元格的一部分。
也可以同时设置两种样式。如果它们使用不同的值设置相同的属性,则 ElementStyle
获胜。
延伸阅读
如果您阅读到这里,说明您对 WPF DataGrid 确实很感兴趣。在这种情况下,我想向您推荐我写的另一篇关于 WPF DataGrid 的文章。