我编写了一个简单的示例应用程序,其中只有一个 XAML 文件和代码隐藏。要重新创建我所做的,只需创建一个新的 WPF 4.5 应用程序,然后将下面的代码粘贴到适当的文件中。
我的解决方案使用视图模型,它允许您使用数据绑定来做所有事情(并且不需要您在代码隐藏中连接事件)。
这可能看起来比您预期的要多得多,但请记住,这是一个完整的示例,其中很多只是设置。对于真正重要的代码,希望您会发现,尽管它加起来有相当多的行数,但它为您提供了一个非常强大的模板,用于在 WPF 中创建各种酷的用户界面。我在每个代码文件之后添加了一些注释,希望能更容易地弄清楚代码的作用。
MainWindow.xaml
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:wpfApplication1="clr-namespace:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow"
Height="350"
Width="525"
d:DataContext="{d:DesignInstance Type=wpfApplication1:MainViewModel, IsDesignTimeCreatable=False}">
<DataGrid AutoGenerateColumns="False"
ItemsSource="{Binding AttributeUpdateViewModels}"
GridLinesVisibility="Vertical">
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Setter Property="BorderThickness"
Value="{Binding BorderThickness}" />
<Setter Property="BorderBrush"
Value="Black" />
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<DataGridTextColumn Header="Number"
Binding="{Binding Number}" />
<DataGridTextColumn Header="Attribute"
Binding="{Binding Attribute}" />
<DataGridTextColumn Header="Old"
Binding="{Binding Old}" />
<DataGridTextColumn Header="New"
Binding="{Binding New}" />
</DataGrid.Columns>
</DataGrid>
</Window>
这基本上只是一个带有文本列的简单数据网格。神奇的是自定义行样式,它根据需要创建水平网格线。 (有关数据绑定的更多详细信息,请参见下文。)
MainWindow.xaml.cs(即代码隐藏):
using System.Collections.Generic;
using System.Linq;
using System.Windows;
namespace WpfApplication1
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
}
public class MainViewModel
{
public List<AttributeUpdateViewModel> AttributeUpdateViewModels { get; set; }
public MainViewModel()
{
var rawAttributeUpdates = new[]
{
new AttributeUpdate { Number = 1, Attribute = "Height", Old = "1.1", New = "0.9" },
new AttributeUpdate { Number = 1, Attribute = "Material", Old = "Steel1", New = "Steel2" },
new AttributeUpdate { Number = 2, Attribute = "Color", Old = "Green", New = "Light-Green" },
new AttributeUpdate { Number = 3, Attribute = "Attribute4", Old = "Old4", New = "New4" },
new AttributeUpdate { Number = 3, Attribute = "Attribute5", Old = "Old5", New = "New5" },
new AttributeUpdate { Number = 3, Attribute = "Attribute6", Old = "Old6", New = "New6" },
new AttributeUpdate { Number = 4, Attribute = "Attribute7", Old = "Old7", New = "New7" },
new AttributeUpdate { Number = 5, Attribute = "Attribute8", Old = "Old8", New = "New8" },
new AttributeUpdate { Number = 5, Attribute = "Attribute9", Old = "Old9", New = "New9" },
new AttributeUpdate { Number = 1, Attribute = "Attribute10", Old = "Old10", New = "New10" }
};
var sortedAttributeUpdates = rawAttributeUpdates.OrderBy(x => x.Number);
var groupedAttributeUpdates = sortedAttributeUpdates
.GroupBy(x => x.Number);
AttributeUpdateViewModels = sortedAttributeUpdates
.Select(x => GetAttributeUpdateRow(x, groupedAttributeUpdates))
.ToList();
}
private AttributeUpdateViewModel GetAttributeUpdateRow(
AttributeUpdate attributeUpdate,
IEnumerable<IGrouping<int, AttributeUpdate>> groupedAttributeUpdates)
{
var lastInGroup = groupedAttributeUpdates.Single(x => x.Key == attributeUpdate.Number).Last();
return new AttributeUpdateViewModel
{
Number = attributeUpdate.Number,
Attribute = attributeUpdate.Attribute,
New = attributeUpdate.New,
Old = attributeUpdate.Old,
IsLastInGroup = attributeUpdate == lastInGroup
};
}
}
public class AttributeUpdate
{
public int Number { get; set; }
public string Attribute { get; set; }
public string Old { get; set; }
public string New { get; set; }
}
public class AttributeUpdateViewModel
{
public int Number { get; set; }
public string Attribute { get; set; }
public string Old { get; set; }
public string New { get; set; }
public bool IsLastInGroup { get; set; }
public Thickness BorderThickness
{
get { return IsLastInGroup ? new Thickness(0, 0, 0, 1) : new Thickness(); }
}
}
}
基本上,我假设您在表格的每一行中显示的数据是AttributeUpdate。 (我只是编造的,你可能有一个更好的名字。)
由于AttributeUpdate 是纯数据,与您的数据应该如何格式化无关,我创建了一个AttributeUpdateViewModel 来组合显示所需的数据和格式信息。
所以,AttributeUpdate 和 AttributeUpdateViewModel 共享相同的数据,但视图模型添加了几个处理格式的属性。
用于格式化的新属性有哪些?
-
IsLastInGroup - 所讨论的行是否是其组的最后一行(其中组中的所有项目共享相同的Number)。
-
BorderThickness - 边界的Thickness。在这种情况下,如果该项目在组中的最后一个,则底部边框为 1,其他所有内容为 0,否则为 0。
数据绑定在 XAML 文件中显示为 {Binding name_of_property},只需在视图模型中挖掘数据和格式信息。如果在您的应用程序运行期间底层数据可能发生变化,您将希望让您的视图模型实现INotifyPropertyChanged interface。 INotifyPropertyChanged 本质上为您的应用程序添加了“更改检测”,允许您的绑定自动重新绑定到新的/更改的数据。
最后一点是我使用了LINQ query 来处理分组逻辑。此特定查询按Number 对行进行排序,然后按Number 对它们进行分组。然后它创建AttributeUpdateViewModel 实例,根据当前AttributeUpdate 是否匹配其组中的最后一项来填充IsLastInGroup。
注意:为了简单起见,我将几个类放在一个文件中。通常的约定是每个文件一个类,因此您可能希望将每个类分解到自己的文件中。
结果
编辑
@Mike Strobel 的评论指出,按数字排序可能不一定是可取的。例如,用户可能希望按不同的列排序,但仍会看到按编号分组的行。我不确定这是否是一个常见的用例,但如果这是一个要求,您可以简单地替换为不同的 LINQ 查询,该查询将“当前”值与“下一个”值进行比较,然后确定 Number 是否更改。这是我的破解之道:
var nextAttributeUpdates = rawAttributeUpdates
.Skip(1)
.Concat(new[] { new AttributeUpdate { Number = -1 } });
AttributeUpdateViewModels = rawAttributeUpdates
.Zip(
nextAttributeUpdates,
(c, n) => new { Current = c, NextNumber = n.Number })
.Select(
x => new AttributeUpdateViewModel
{
Number = x.Current.Number,
Attribute = x.Current.Attribute,
New = x.Current.New,
Old = x.Current.Old,
IsLastInGroup = x.Current.Number != x.NextNumber
})
.ToList();