LINQ、group by 和 WPF 数据绑定
WPF 数据绑定与 LINQ 配合得非常好!本文将介绍如何使用 LINQ 的 group by 子句创建分层结果集,以及如何在 WPF 数据绑定中使用它。
引言
LINQ 为 C# 编程语言带来了许多出色的功能,而这些功能是数据库开发人员多年来一直知晓的。在某些方面,LINQ 远远超出了 SQL 所提供的功能。其中一个例子是 group by
子句。在 SQL 中,group
操作的结果返回一个表——除此之外不可能有其他结果,因为 SQL 不了解类的概念。相反,LINQ 的 group by
操作可以创建分层结果结构。本文将介绍如何使用 group by
编写 LINQ 查询,更重要的是,还将演示如何在 WPF 中使用数据绑定来使用该结果。
LINQ 查询
在示例中,我们假设我们收集计算机上运行的程序的事件状态。对于每个事件,我们可以收集进程 ID、进程描述(例如,exe 的文件名)以及事件时间。以下类充当事件的容器
public class Event
{
public int PID { get; set; }
public string Desc { get; set; }
public DateTime EventTime { get; set; }
}
以下代码行生成了一些演示数据
var data = new List<Event>()
{
new Event() { PID = 1, Desc="Process A", EventTime = DateTime.Now.AddHours(-1) },
new Event() { PID = 1, Desc="Process A", EventTime = DateTime.Now.AddHours(-2) },
new Event() { PID = 2, Desc="Process B", EventTime = DateTime.Now.AddHours(-3) },
new Event() { PID = 2, Desc="Process B", EventTime = DateTime.Now.AddHours(-4) },
new Event() { PID = 3, Desc="Process C", EventTime = DateTime.Now.AddHours(-5) }
};
正如你所见,进程的主数据被多次存储(即,数据结构不处于范式)。我们的 LINQ 查询应该返回一个分层结果,其中每个进程只包含一次。此外,对于每个进程,我们希望有一个相应的事件集合。解决此问题的 LINQ 查询如下所示
var result =
from d in data
group d by new { d.PID, d.Desc } into pg
select new { Process = pg.Key, Items = pg };
注意 group by
子句的写法以及结果(匿名类型)的构建方式。该查询按进程 ID 和进程描述对结果进行分组。这两个字段共同构成了复合分组表达式(new { d.PID, d.Desc }
)。pg
的类型为 IGrouping<TKey, TElement>
。TKey
表示前面提到的分组表达式。IGrouping
实现 IEnumerable
。因此,pg
可以在 select
子句中使用,以便为每个组嵌入相应的 Event
对象列表。
请注意,结果的匿名类型包含每个列的名称(Process = pg.Key, Items = pg
)。这很重要,因为没有名称,WPF 中的数据绑定会困难得多(事实上,我不知道没有名称是否可能)。
以下是在 Visual Studio 调试器中查看结果的方式
WPF 数据绑定
在我的示例中,我也想在用户界面中表示分层结果结构。因此,以下示例应该为每个键创建一个展开器控件。在展开器内部,它应该显示一个列表框,其中包含相应键的事件详细信息。以下是我想要实现的结果的屏幕截图
使用数据绑定创建展开器控件非常简单
<Window x:Class="WpfApplication5.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="550">
<Grid>
<ItemsControl x:Name="TopLevelListBox" ItemsSource="{Binding}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Expander ExpandDirection="Down" Width="175">
<Expander.Header>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Process.PID}" Margin="0,0,5,0"/>
<TextBlock Text="{Binding Path=Process.Desc}" />
</StackPanel>
</Expander.Header>
</Expander>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Window>
正如你所见,我使用自定义 ItemsPanel
水平显示展开器控件。数据模板将每个结果对象转换为展开器控件。为了使数据绑定生效,我们不能忘记设置 ItemsControl
的数据上下文
var result =
from d in data
group d by new { d.PID, d.Desc } into pg
select new { Process = pg.Key, Items = pg };
TopLevelListBox.DataContext = result;
在此基础上,我们可以使用 LINQ 查询生成的层级结果,为每个展开器控件插入事件列表,而无需编写任何额外的 C# 代码
<Window x:Class="WpfApplication5.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="550">
<Grid>
<ItemsControl x:Name="TopLevelListBox" ItemsSource="{Binding}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Expander ExpandDirection="Down" Width="175">
<Expander.Header>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Process.PID}" Margin="0,0,5,0"/>
<TextBlock Text="{Binding Path=Process.Desc}" />
</StackPanel>
</Expander.Header>
<ListBox x:Name="SubListBox" ItemsSource="{Binding Path=Items}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=EventTime}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Expander>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Window>
请注意,列表框 SubListBox
绑定到 Items
。Items
已定义为 LINQ 查询结果类型中的一个字段。它包含每个分组键对应的事件列表。通过这样绑定,我们可以在列表框的 DataTemplate
中访问 Event
的属性。
摘要
在我看来,此示例的重要收获是
- LINQ 是一个强大的工具,不仅在与数据库结合使用时有用。它还可以更轻松地处理内存中的对象结构。
- LINQ 查询可以创建比 SQL 更复杂的结果结构。
- WPF 数据绑定与 LINQ 结果配合得非常好,即使是分层结果结构。