MVVM 项目中的动态 DataGrid 单元格样式
在 MVVM 项目中动态更改 DataGrid 单元格样式
引言
这个小程序演示了一种根据单元格内容动态修改 DataGrid
单元格样式的方法。动态样式的一个例子是:如果单元格中的值变为负数,您可能希望将单元格的背景颜色更改为红色。此样式以模型-视图-视图模型 (MVVM) 模式为重点进行了演示。我在此处提供了完整的 Visual Studio 源代码和其他项目文件 此处。项目的可运行示例可从 此处 下载。在应用程序中,每次用户单击 [更改值] 时,DataGrid
中的单元格都会填充 1 到 9 之间的新的随机整数。单元格的背景颜色会根据单元格的新内容而变化。
必备组件
该解决方案使用 Visual Studio 2019 Community Edition(版本 16.3.9)和 .NET 4.7.2 构建。它还需要 Expression.Blend.Sdk
版本 1.0.2,但此 SDK 已随项目文件一起打包。
假定读者对 C# WPF 项目和 MVVM 模式有基本了解。
Using the Code
构建并运行代码后,将出现以下窗口
注意:单元格中的整数由随机生成器生成,每次运行代码时以及每次单击 [更改值] 按钮时都会有所不同。我意识到颜色有点刺眼,但目的是清楚地展示 DataGrid
单元格的背景颜色在内容更改时是如何改变的。它无意遵循微软对用户界面样式的平淡指南。
动态单元格样式是通过一个 MultiValueConverter
完成的。此 Converter
是项目 View
中名为 CellColorConverter.cs
的文件的一部分。这些转换器要求将一个对象数组传递给它们,转换器将基于该数组进行处理。在我们的例子中,这个对象数组将包含两个对象:一个 DataGridCell
和包含该单元格的 DataGrid
中的 DataRow
。
每个单元格包含一个 1 到 9 之间的随机整数。每次单击 [更改值] 时,这些数字都会随机变化。单元格的背景颜色也会根据下面的详细信息而变化。
在上面的示例中,"First
" 列中所有单元格的背景颜色都设置为系统颜色 Colors.LightGoldenrodYellow
,而与单元格内容无关。对于 "Second
" 列到 "Eighth
" 列中的所有单元格,背景颜色设置如下:
如果单元格包含数字 1
、2
或 3
,背景颜色将设置为系统颜色 Colors.LightGreen
。
如果单元格包含数字 4
、5
或 6
,背景颜色将设置为系统颜色 Colors.LightSteelBlue
。
如果单元格包含数字 7
、8
或 9
,背景颜色将设置为系统颜色 Colors.LightSalmon
。
对于 "First
" 列中的单元格,字体大小设置为 20,字体样式设置为斜体。当然,这可以通过更简单的方式实现,但这里的目的是展示 Converter
如何影响字体设置等其他属性的更改。
首先,我们需要了解 Converter
如何与项目关联。
查看主 XAML 文件:
MainWindow.xaml,在 <Window.Resources>
标签下。在这里,转换器被列为一个资源,其 Key 为 "ColorConverter"
。
<local:CellColorConverter x:Key="ColorConverter" />
现在向下看一点,DataGrid
样式定义所在的位置。
<Style x:Key="BlueDataGridStyle" TargetType="{x:Type DataGrid}">
<Setter Property="CellStyle" Value="{DynamicResource BlueDataGridCellStyle}" />
.................................
BlueDataGridCellStyle
中的单元格背景颜色定义如下:
<Setter Property="Background">
<Setter.Value>
<MultiBinding Converter="{StaticResource ColorConverter}">
<MultiBinding.Bindings>
<Binding RelativeSource="{RelativeSource Self}" />
<Binding Path="Row" />
</MultiBinding.Bindings>
</MultiBinding>
</Setter.Value>
</Setter>
在这里,您可以看到两个对象被传递给了 Converter
,首先是单元格本身,然后是包含该单元格的行。
<Binding RelativeSource="{RelativeSource Self}" />
<Binding Path="Row" />
关于单元格的说明:传递给 Converter
的单元格不带其内容。您可能会在 Converter
中尝试访问单元格的内容,如下所示:Cell.Content
,这在语法上是正确的,但它将始终返回 null
。稍后将详细介绍这一点。
因此,当系统需要渲染单元格的背景时,它会发现需要将单元格及其行传递给 Converter
,而 Converter
将返回用于背景的颜色。
转换器
现在是时候看看这个非常重要的 Converter
了。任何 MultiValueConverter
都必须遵守 IMultiValueConverter
接口规定的约定。此接口规定 Converter
代码应包含两个方法:
object Convert(object[] values, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
和
object[] ConvertBack(object value, Type[] targetType,
object parameter, System.Globalization.CultureInfo culture)
通常,ConvertBack
方法不做任何事情,但必须提供以满足接口。Convert(...)
方法的第一个参数 object[]
将由正在渲染的单元格和包含该单元格的 DataGrid
行组成。此方法的其他参数在此处未使用。
这是 Converter
的完整 Convert(...)
方法:
public object Convert(object[] values, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
if (values[0] is DataGridCell cell && values[1] is DataRow row)
{
try
{
string columnName = (string)cell.Column.Header;
int content = row.Field<int>(columnName); // Header must be same as column name
int columnIndex = cell.Column.DisplayIndex;
if (columnIndex == 0)
{
cell.FontStyle = FontStyles.Italic;
cell.FontWeight = FontWeights.Bold;
cell.FontSize = 20;
return new SolidColorBrush(Colors.LightGoldenrodYellow);
}
if (content < 4)
{
return new SolidColorBrush(Colors.LightGreen);
}
if (content > 6)
{
return new SolidColorBrush(Colors.LightSalmon);
}
return new SolidColorBrush(Colors.LightSteelBlue);
}
catch (Exception)
{
return new SolidColorBrush(Colors.Black); // Error! An Exception was thrown
}
}
return new SolidColorBrush(Colors.DarkRed); // Error! object[] is invalid.
}
首先,我们需要验证传递给 Converter
的 object[]
是否确实是由 DataGridCell
和 DataRow
组成的,并同时为这两个对象指定名称。
if (values[0] is DataGridCell cell && values[1] is DataRow row)
由于我们无法直接访问单元格的 content
,因此我们从与该单元格对应的行的 Field
获取它。
string columnName = (string)cell.Column.Header;
int content = row.Field<int>(columnName); // Header must be same as column name
content
现在包含传递给 Converter
的 DataGrid
单元格中的整数。
注意:为了使此方法奏效,DataGrid
列标题必须与列名相同。这通常不是问题。
接下来,Converter
获取 Column
的 DisplayIndex
,如果索引为零(最左边的列),Converter
会对单元格字体进行一些更改,并返回系统颜色 Colors.LightGoldenrodYellow
作为单元格背景。这就是将整列设置为相同的背景颜色的方法。
int columnIndex = cell.Column.DisplayIndex;
if (columnIndex == 0)
{
cell.FontStyle = FontStyles.Italic;
cell.FontWeight = FontWeights.Bold;
cell.FontSize = 20;
return new SolidColorBrush(Colors.LightGoldenrodYellow);
}
接下来,对于列索引大于零的情况,当 content
小于 4
时,Converter
返回 Colors.LightGreen
;当 content
大于 6
时,返回 Colors.LightSalmon
;对于值 4
、5
和 6
,返回 Colors.LightSteelBlue
。
if (content < 4)
{
return new SolidColorBrush(Colors.LightGreen);
}
if (content > 6)
{
return new SolidColorBrush(Colors.LightSalmon);
}
return new SolidColorBrush(Colors.LightSteelBlue);
DataGrid 绑定
我们如何将整数放入 DataGrid
的单元格中?如果您查看 ViewModel
(MainViewModel.cs),您会看到一个名为 ValuesArray
的 DataTable
,其列名与 DataGrid
的列名相同。在 View
(MainWindow.xaml) 中,您将看到窗口的 DataContext
设置为: MainViewModel
:
xmlns:viewmodel="clr-namespace:DataGridProject.ViewModel"
和
<Window.DataContext>
<viewmodel:MainViewModel />
</Window.DataContext>
同样,在 MainWindow.xaml 中 DataGrid
的定义中:
ItemsSource="{Binding Path=ValuesArray}"
这将 DataGrid
单元格中的值绑定到 DataTable ValuesArray
中相应单元格的值。换句话说,View
和 ViewModel
之间的耦合非常松散。ViewModel
对 View
中的任何控件都没有直接的了解,这符合 MVVM 模式的原则。
按钮绑定
同样,这两个按钮通过绑定与 ViewModel
中的方法松散耦合。当您单击 [更改值] 时,通过绑定将执行 ViewModel
中的以下方法:
private void ChangeValues()
{
DataRow tableRow;
int row;
Random rnd = new Random();
ValuesArray.Rows.Clear();
for (row = 0; row < 16; row++)
{
tableRow = ValuesArray.NewRow();
tableRow.SetField<int>("First", rnd.Next(1, 10));
tableRow.SetField<int>("Second", rnd.Next(1, 10));
tableRow.SetField<int>("Third 3", rnd.Next(1, 10));
tableRow.SetField<int>("Fourth", rnd.Next(1, 10));
tableRow.SetField<int>("Fifth", rnd.Next(1, 10));
tableRow.SetField<int>("Sixth", rnd.Next(1, 10));
tableRow.SetField<int>("Seventh", rnd.Next(1, 10));
tableRow.SetField<int>("Eighth", rnd.Next(1, 10));
ValuesArray.Rows.Add(tableRow);
}
}
ViewModel
将整数保存在 DataTable ValuesArray
中。它“不知道”这些值通过绑定传输到 View
。
结论
我花了一些时间才弄清楚如何使用 MultiValueConverter
。我的目标是将所有与 DataGrid
单元格样式相关的操作都放在 View
中执行,并让 ViewModel
不参与这些操作。
我希望这篇文章能对你们中的一些人有所帮助。
历史
- 2019 年 11 月 18 日:初版