得益于样式、数据模板以及控件模板提供的扩展功能,即使ListBox 控件(以及类似的ComboBox 控件)也可成为以各种方式显示数据的强大工具。然而,某些类型的数据表示形式只凭ListBox控件很难实现。幸运的是,WPE还提供了几个填补这一空白的富数据控件,包括以下几个控件:
- ListView。ListView 继承自简单的没有特色的 ListBox。增加了对基于列显示的支持,并增加了快速切换视图或显示模式的能力,而不需要重新绑定数据以及重新构建列表。
- TreeView。TreeView时层次化容器,这意味个创建多层数据显示。例如,可创建在第一级中显示类别组,并在每个类别节点中显示相关产品的TreeView控件。
- DataGrid。DataGrid是WPF中功能最完备的数据显示工具。它将数据分割到包含行和列的网格中,就像ListView控件,但DataGrid控件具有其他格式化特性(比如冻结列以及设置单行样式的能力),并且支持就地编辑数据
ListView控件
ListView 类是一个特殊的列表类,它是专门针对显示相同数据的不同视图而设计的。如果需要构建显示每个数据项几部分信息的多列视图,ListView控件特别有用。
ListView 类继承自 ListBox类,并使用 View属性进行了扩展。View 属性是另一个扩展点,用于创建内容丰富的列表。如果没有设置 View 属性,ListView 控件的行为就类似于其功能较少的祖先 ListBox 控件的行为。然而,如果提供用于指示如何设置数据项格式和样式的视图对象,ListView 控件就变得更有趣了。
从技术角度看,View 属性指向继承自 VewBase 类(它是一个抽象类)的任意类的实例。ViewBase类非常简单——实际上,它只不过是一个将两个样式捆绑在一起的包。其中的一个样式应用到ListView 控件(并通过 DefaultStyleKey 属性加以引用),而另一个样式应用到 ListView 控件中的项(并通过 ItemContainerDefaultStyleKey属性加以引用),DefaultStyleKey 和 ItemContainerDefaultStyleKey属性实际上没有提供什么样式,它们只返回指向样式的ResourceKey 对象。
**ListBox控件已经提供了强大的数据模板和样式化功能(所有继承自ItemsControl的类都是如此)。**有雄心的开发人员个通过提供不同的数据模板、布局面板已经控件模板,成型实现ListView控件的格式化外观。
事实上,为创建能够自定义的具有多列的列表,不需要使用具有 View 属性的 ListView 类。实际上,为 ListBox 控件使用模板和样式化功能可获得相同的效果。但 View 属性是一个很有用的抽象概念。下面列出它的一些优点
:
- 可重用的视图。ListView控件将所有特定于视图的细节分离到一个对象中,这样便于创建不依赖于数据并且个用于多个列表的视图。
- 多视图。将ListView控件和View对象分离开来,还使得对同一列表切换多个视图变得更容易(例如,在Windows资源管理器中,使用这种技术显示文件和文件夹的不同查看方式)。个通过动态改变模板和样式男构建相同的特性,但只使用一个封装了所有视图细节的对象更容易。
- 更好的组织。视图对象封装了两个样式:一个用于ListView根控件;另一个用于列表中的单个项。因为这些样式封装在一起,所以很明显这两部分时相关联的,个共享特定的细节并且相互依赖。例如,对于基于列的ListView控件这是非常合理的,因为需要保持列标题和列数据相互对齐
使用这种模型,可预先创建大量非常有用的、所有开发人员都可以使用的视图。但WPF目前只提供了视图对象 GirdView。尽管 GridView 对于创建多列列表非常有用,但如果有其他需求的话,就需要创建自定义视图。接下来的几节将介绍这两部分内容。
如果希望实现可配置的数据显示,并希望用户能够选用网格风格的视图,那么 GridView 控件是很好的选择。但如果希望网格支持高级的样式化、选择以及编辑功能,就需要使用 DataGrid 控件。
使用GridView创建列
GridView类继承自ViewBase类,表示具有多列的列表视图。通过为GridView.Columns
集合添加GridViewColumn
对象可定义这些列。
GridView 和 GridViewColumn 都提供了一些有用的方法,可使用这些方法定制列表的显示外观。为创建最简单、最直观的列表(很像 Windows 资源管理器中的详细信息视图),只需要为每个 GridViewColumn 对象设置两个属性:Header 和 DisplayMemberBinding。Header 属性提供放在列顶部的文本,而 DisplayMemberBinding属性包含一个绑定,该绑定从每个数据项提取希望显示的信息。
下面具有三列有关产品的信息
1 | <Grid> |
这个示例有几个非常重要的地方需要注意。首先,所有列都没有硬编码尺寸。GridView 视图只是将列的尺寸设置得足够大以适应最宽的可见项(或列标题,如果它更宽的话),在 WPF 的流式布局中,这种设计是非常合理的(当然,如果具有非常大的列,这也会带来一定的麻烦。对于这种情况,可选择将文本封装起来,如后面的“2.单元格模板”部分所述)。
此外,还需要注意如何使用功能完整的绑定表达式设置DisplayMemberBinding
属性。
改变列的尺寸
最初,GridView视图使每列足够宽以适应最大的可见值。然而,可通过单击和拖动列标题的边缘,很容易地改变任意列的尺寸。也可双击列标题的边缘,强制GridViewColumn 对象根据当前的可见内容改变自身的大小。例如,如果向下滚动列表,就会发现一个被截断的项,因为它比当前列更宽。只要双击正确的列标题边缘,这个列就会自动地扩展自己以适应内容。
为更精确地控制列的尺寸,当声明列时个设置特定宽度:
1 | <GridViewColumn Width="300" .../> |
这只是确定了列的初始宽度,不会阻止用户使用上面介绍的其他技术重新改变列的尺寸。但 GridViewColumn 类没有定义诸如 MaxWidth与 MinWidth 的属性,所以无法约束如何改变列的尺寸。如果希望完全禁止改变列的尺寸,唯一的选择是为GridViewColumn 的标题提供新模板。
单元格模板
为在单元格中显示数据,GridViewColumn.DisplayMemberBinding
属性不是唯一的选择。另一个选择是使用CellTemplate
属性,该属性使用数据模板。除了只能应用于一列之外,它与之前学过的数据模板十分相似。如果很耐心的话,也可为每一列都提供数据模板。
当自定义GridView
视图,单元格模板是关键的一部分,它的一个功能时允许文本换行。通常,列中的文本被封装到单行的TextBlock元素中。然而,可很容易地使用自己设计的数据模板改变这个细节:
1 | <GridViewColumn Header="Description" Width="300"> |
注意,为了能够得到换行效果,需要使用 Width 属性限制列宽。如果用户改变了列的尺寸文本会重新换行以适应新的宽度。您可能不希望限制 TextBlock 的宽度,因为这会使文本被限制为特定的尺寸,而不管列变得多宽或多窄。
如果能够创建使用 DisplayMemberBinding属性的数据模板,将是非常有用的。这样一来,可使用 DisplayMemberBinding 提取所需的特定属性,并使用 CellTemplate 属性将内容格式化成合适的可视化表示形式。但这是不可能实现的。如果同时设置了DisplayMember和 CellTemplate 属性,GridViewColumn 就会使用 DisplayMember 属性为单元格设置内容并完全忽略模板。
数据模板并不局限于只能使用 TextBlock 的属性。也可使用数据模板提供完全不同的元素。例如,下面的列使用数据模板显示图像
1 | <GridViewColumn Header="Picture"> |
可改变模板,从而为不同的数据项应用不同的模板。为此,需要创建模板选择器,根据位于特定位置的数据对象的属性选择恰当的模板。为使用该特性,需要创建自己的选择器,并使用它设置 GridViewColumn.CelTemplateSelector 属性。
如果希望用自己的内容填充列标题,但不希望为每列单独指定内容,可使用GridViewColumn.HeaderTemplate 属性定义一个数据模板。这个数据模板可以绑定到在GridViewColumn.Header属性中指定的任何对象,并相应地加以显示。
创建自定义视图
如果GridView视图不能满足需要,个创建自己的视图以扩展ListView
控件的功能。不过,这并不容易实现。
为理解这一点,需要理解有关视图工作原理的更多细节。视图通过重写两个受保护的属性进行工作:DefaultStyleKey和 ItemContainerDefaultStyleKey。这两个属性都返回名为ResourceKey的特殊对象,该对象指向在 XAML中定义的样式。DefaultStyleKey属性指向将被用于配置整个 ListView 控件的样式,而 ItemContainer.DefaultStyleKey属性指向将被用于配置 ListView 控件中每个 ListViewItem 元素的样式。尽管这些样式可修改任意属性,但它们通常通过替换用于ListView 控件:的 ControlTemplate 以及用于每个 ListViewItem 元素的 DataTemplate 进行工作。
这正是出现问题的地方。用于显示项的 DataTemplate 是在 XAML, 标记中定义的。假设希望创建一个 ListView 控件,为每个项显示平铺的图像。使用数据模板十分容易——只需要将Image 元素的 Source 属性绑定到数据对象的合适属性即可。但如何知道用户会提供哪个数据对象呢? 如果将属性名称硬编码为视图的一部分,就会使视图失去用处,从而会限制在其他情况下重用自定义的视图。另一种选择是——强制用户提供 DataTemplate–这意味着不能在视图中封装太多功能,这样重用视图也就失去了意义。
在开始创建自定义视图之前,首先需要考虑一下:通过为 ListBox 控件简单使用合适的DataTemplate,或者结合使用 ListView与 GridView,是否能够得到同样的结果。
如果通过重新样式化 ListView 控件(甚至 ListBox 控件)已经能够得到所有需要的功能,那么为什么还要设计自定义视图呢?主要原因是希望列表能够动态地改变视图。例如,可能希望根据用户的选择以不同模式查看产品列表。可通过动态地切换不同的 DataTemplate 对象实现这种效果(这也是合理的方法),但视图通常需要同时改变 ListViewItem的 DataTemplate,以及 ListView控件自身的布局或整个外观。视图有助于澄清源代码中这些细节之间的关系。
下面的示例显示了如何创建能无缝地从一个视图切换到另一个视图的网格控件。该网格以熟悉的独立列视图开始,但还支持两个平铺显示图像的视图
视图类
构建这个示例的第一步是设计代表自定义视图的类。该类必须继承自ViewBase 类。此外,为提供样式引用,自定义视图类通常会(但并非总是如此)重写 DefaultStyleKey和 ItemContainerDefaultStyleKey 属性。
在这里示例中,视图被命名为TileView
,因为它的重要特征是在提供的空间中平铺显示数据项。TileView
类使用WrapPanel
面板布局所包含的ListViewItem
对象。这个视图没有被命名为ImageView,这是因为没有硬编码平铺显示的内容,并且完全可以不包含图像。相反,平铺的内容时由开发人员在使用TileView
视图时提供的模板定义的。
TileView 类应用了两个样式:TileView(用于 ListView)和 TileViewItem(用于 ListViewItem)。此外,TileView 类还定义了 ItemTemplate 属性,使得使用 TileView 类的开发人员能够提供正确的数据模板。然后这个模板被插入到每个 ListViewltem 元素的内部,用于创建平铺显示的内容。
1 | public class TileView : ViewBase |
正如您可能看到的,TileView类没有执行很多工作。它只提供了一个ComponentResourceKey弓用,指向正确的样式。在第10章当第一次分析如何从 DLL程序集中检索共享资源时,已学习过ComponentResourceKey.
**ComponentResourceKey封装了两部分信息:拥有样式的类的类型,以及标识资源的描述性的 ResourceId 字符串。在这个示例中,类型很明显是针对这两个资源键的 TileView类。描述性的 Resourceld 名称不很重要,但必须保持一致,在这个示例中,默认的样式键被命名为 TileView而用于每个 ListViewltem 元素的样式被命名为 TileViewItem。**接下来的内容将深入分析这些样式,并会介绍如何定义它们。
视图样式
为让 TileView
视图能够工作,WPF 需要能够找到希望使用的样式。确保能够自动获得样式的技巧是创建名为 generic.xaml
的资源字典。这个资源字典必须被放在项目文件夹下的 Themes
子文件夹中。WPF 使用 generic.xaml 文件获取关联到某个类的默认样式
在这个示例中,generic.xaml 文件定义的样式被关联到 TileView 类。为在样式和 TileView类之间设置关联,需要在 generic.xaml 资源字典中为样式设置正确的键。不能使用普通的字符串键,WPF 期望设置的键是一个 ComponentResourceKey对象,并且这个 ComponentResourceKey对象需要与 TileView类中的 DefaultStyleKey和 ItemContainerDefaultStyleKey属性返回的信息相匹配
下面是包含正确键的
generic.xaml
资源字典的基本结构
正如您可能看到的,为每个样式设置的键都与 TileView 类提供的信息相匹配。此外,样式还设置了 TargetType 属性(指示样式会修改哪些元素)和 BasedOn 属性(指示继承 ListBox 控件和ListBoxltem 元素使用的更基本的样式)。这会节省一些工作,并使您能集中精力使用自定义的设置扩展这些样式。
因为这两个样式都与 TileView 类关联到一起,所以无论何时将 View 属性设置为 TileView 对象,它们都将被用于配置 ListView 控件。如果正在使用不同的视图对象,将会忽略这些样式。这正是所希望的 ListView控件的工作方式,从而每当改变 View属性时都会无缝地重新配置ListView控件。
1 | <ResourceDictionary |
应用于ListView控件的TileView样式有如下三个变化:
- 在
ListView
控件周围添加了稍微不同的边框 - 将
Grid.IsSharedSizeScope
附加属性设置为true。从而如果使用Grid布局容器,可以使不同的列表项使用共享的列或行。在这个示例中,这个设置确保在详细信息平铺视图中每个项都具有相同的范围 - 将
ItemsPanel
属性成StackPanel面板改为WrapPanel
面板,从而实现平铺行为。WrapPanel
面板的宽度被设置为与ListView
控件的开盾相匹配。
请记住,为了确保最大的灵活性,将 TileView 类设计为使用由开发人员提供的数据模板。为应用这个模板,TileView 样式需要检索 TileView 对象(使用 ListView.View 属性),然后从TileView.ItemTemplate 属性中提取数据模板。这个步骤使用绑定表达式来查找元素树(使用FindAncestorRelativeSource 模式),直至找到所属的 ListView 控件。
使用ListView控件
一旦构建好视图类和支持样式,就为在ListView
控件中使用它们做好了准备。为使用自定义视图,只需将ListView.View
属性设置为自定义视图对象的实例,如下所示:
1 | <Window |
1 | public partial class MainWindow : Window |
毫无疑问,为生成具有多个视图选项的 ListView控件,需要的代码比想象得多。然而,现在已经完成了该例,并且可以很容易地(以 TileView 类为基础)创建其他提供不同数据项模板的视图,从而提供更多的视图选项。
为视图传递信息
通过添加当使用视图时使用者可设置的属性,可增加视图类的灵活性。然后样式可以使用数据绑定检索这些数值,并使用它们配置 Setter对象
例如,现在 TileView 类使用不是很吸引人的蓝色来突出显示被选中的项。这个效果更加令人惊奇,因为它使显示产品细节的黑色文本更难以阅读。可通过使用自定义的具有适当触发器的自定义控件模板可以纠正这个问题。
但并非硬编码一套更好的颜色,让视图使用者指定这个细节更合理。为让TileView
类实现这个目标,需要添加与下面类似的一组属性:
1 | public class TileView : ViewBase |
当实例化视图对象时,可以设置这些细节了:
1 | <customViews:TileView |
最后一步时在ListViewItem
样式中使用这些颜色。为此,需要添加替换ControlTemplate
的Setter
对象。在这个示例中,结合使用ContentPresenter
元素和简单的圆角边框。当项被选中时,会引发触发器并应用新的边框和背景色:
1 | <Style |
但这种向视图传递信息的技术仍然不能帮助用户实现真正通用的视图,这是因为无法根据这一信息修改数据模板。
TreeView控件
TreeView
控件时最重要的Windows控件之一,它被集成到Windows资源管理器乃至.NET帮助文档库等各种元素中。WPF中实现的TreeView
控件更时令人印象深刻,因为它完全支持数据绑定。
TreeView 控件在本质上是驻留 TreeViewItem 对象的特殊 ItemsControl控件。但与 ListViewItem对象不同,TreeViewItem对象不是内容控件。相反,每个TreeViewItem 对象都是单独的ItemsControl控件,可以包含更多 TreeViewItem 对象。通过这一灵活性,可以创建更深层次的数据显示。
从技术角度看,TreeViewItem 类继承自 HeaderedItemsControl类,HeaderedItemsControl 类又继承自 ItemsControl 类。HeaderedItemsControl 类添加了Header 属性,该属性包含了希望为树中每个项显示的内容(通常是文本)。WPF还另外提供了两个HeaderedltemsControl类:Menultem和 ToolBar.
下面是一个非常脚本的TreeView控件的骨架,它全部使用标记声明:
1 | <TreeView> |
不见得非要使用 TreeViewItem 对象构造 TreeView 控件。实际上,几乎可为 TreeView 控件添加任何元素,包括按钮、面板以及图像。然而,**如果希望显示非文本内容,最好使用TreeViewItem 封装器,并通过 TreeViewItem.Header 属性提供内容。**虽然这与直接为 TreeView控件添加非 TreeViewtem 元素得到的效果相同,但这样做将更容易管理一些特定于 TreeView 控件的细节,例如选择和展开节点。如果希望显示非 UIEement 对象,可使用具有 HeaderTemplate 或HeaderTemplateSelector 属性的数据模板设置其格式。
创建数据绑定的TreeView控件
通常,不希望在标记中使用硬编码的固定信息填充TreeView
控件。相反,将通过代码构造TreeViewItem
对象,或使用数据绑定显示对象集合。
使用数据填充TreeView控件非常简单–与任意 ItemsControl控件一样,只需要设置 ItemsSource 属性。然而,这种技术只能填充 TreeView 控件的第一层。使用TreeView 控件更有趣的方法是包含具有某种嵌套结构的层次化数据。
TreeView 控件使得显示层次化数据变得更加容易,不管是使用手工编写的类还是使用ADO.NETDataSet。只需要指定正确的数据模板,数据模板指示不同层次数据之间的关系。
1 | <TreeView ItemsSource="{Binding Categories}" Margin="5"> |
1 | [ ] |
即时创建节点
TreeView 控件经常用于包含大量数据,这是因为TreeView 控件的显示是能够折叠的。即使用户从顶部滚动到底部,也不需要显示全部信息。完全可在 TreeView 控件中省略不显示的信息,以便降低开销(以及填充树所需的时间)。甚至更好的是,当展开每个TreeViewltem 对象时会引发 Expanded 事件,并且当关闭时会引发Collapsed事件。可通过处理这两个事件即时填充丢失的节点或丢弃不再需要的节点。这种技术被称为即时创建节点
(iust-in-time node creation)。
即时创建节点可应用于从数据库提取所需数据的应用程序,但典型的例子是目录浏览应用程序。现在,大多数用户都有层次复杂的庞大硬盘驱动器。尽管可使用硬盘的目录结构填充TreeView 控件,但这个过程非常缓慢。更好的做法是首先填充部分折叠的视图,并允许用户进入到特定目录。当打开每个节点时,相应的子目录被添加到树中——这个过程几乎是瞬间完成的。
1 | <TreeView Name="treeFileSystem" TreeViewItem.Expanded="TreeFileSystem_OnExpanded" /> |
1 | public partial class MainWindow : Window |
现在,每次展开相应的项时该代码都会执行一次刷新。此外,也可仅在第一次展开时(这时会发现占位符)才进行刷新。这虽然能减少应用程序所需要做的工作,但却会增加使用过时信息的机会。此外,可通过处理 TreeViewItem.Selected 事件,当每次选中一个项时进行刷新;或者当添加、删除或重命名文件夹时,使用组件(如 System.IO.FileSystemWatcher)等待操作系统通知。当发生变化时,FileSvstemWatcher是唯一能够确保立即更新目录树的方法,但这种做法的开销也是最大的。
DataGrid控件
顾名思义,DataGrid 控件是用来显示数据的控件,从对象集合获取信息并在具有行和单元格的网格中显示信息。每行和单独的对象相对应,并且每列和对象的某个属性相对应。
DataGrid 控件添加了许多在 WPF 中处理数据所需的技能。其基于列的模型提供了显著的格式化灵活性。其选择模型允许选择一行、多行或一些单元格的组合。其编辑支持非常强大,可使用 DataGrid 控件作为简单数据和复杂数据的统一数据编辑器。
为创建暂且应急的 DataGrid 控件,可使用自动列生成功能。为此,需要将AutoGenerateColumns 属性设置为 true(这是默认值):
1 | <DataGrid AutoGenerateColumns="True" ItemsSource="{Binding Products}" /> |
为显示非字符串属性,DataGrid 控件调用 ToSting()方法。对于数字、日期以及其他简单类型,该方法效果不错。但如果对象包含更复杂的数据对象,该方法就行不通了(对于这种情况可能希望明确地定义列,从而获得绑定到子属性、使用值转换器或应用模板以获取正确显示内容的机会)。
下表列出了一些用于定制DataGrid
控件基本外观的属性:
名称 | 说明 |
---|---|
RowBackground AlternatingRowBackground | 用于绘制每行背景的画刷(RowBackground),并且决定是否使用不同的背景颜色(AlternatingBackground)绘制交替行,从而更容易区分行。在默认情况下,DataGrid控件为奇数行提供白色背景,为偶数行提供淡灰色背景 |
ColumnHeaderHeight | 位于DataGrid控件顶部的列表题行的高度(设备无关单位) |
RowHeaderWidth | 具有行题头的列的宽度(设备无关单位)。该列在网格的最左边,不显示任何数据该列(使用箭头)指示当前选择的行,并且(使用圈住的箭头)指示正在编辑的行 |
ColumnWidth | 作为 DataGridLength 对象,用于设置每列默认宽度的尺寸改变模式 |
RowHeight | 每行的高度。如果准备在 DataGrid 控件中显示多行文本或不同的内容,该设置很有用。与列不同,用户不能改变行的尺寸 |
GridLinesVisibility | 确定是否显示网格线的DataGridGridlines枚举值(Horizontal、Vertical、None或All) |
VerticalGridLinesBrush | 用于绘制列之间网格线的画刷 |
HorizontalGridLinesBrush | 用于绘制行之间网格线的画刷 |
HeadersVisibility | 确定显示哪个题头的DataGridHeaders枚举值(Column、Row、All、None) |
HorizontalScrollBarVisibility VerticalScrollBarVisibility | 确定是否显示滚动条的ScrollBarVisility枚举值:当需要时显示(Auto)、总是显示(Visible)或总是不显示(Hidden)。这两个属性的默认值都是Auto |
改变列的尺寸与重新安排列
当显示自动生成的列时,DataGrid
控件尝试根据DataGrid.ColumnWidth
属性智能地改变每列的宽度。
为设置ColumnWidth
属性,需要提供DataGridLength
对象。DataGridLength
对象能够指定确切的尺寸(使用设备无关单位),或指定特定的尺寸改变模式,从而让DataGrid
控件自动完成一些工作。
- DataGridLength.SizeToHeader:使列足够宽以适应它们的题头文本
- DataGridLength.SizeToCells:该模式加宽每一列以适应当前视图中最宽的值
- DataGridLength.Auto:该模式的工作方式和
DataGridLength.SizeToCells
模式类似,除了加宽每列以适应最大的显示值或列题头文本——使用其中较大值 - 星号(*):与在Grid布局面板中使用的星号(
*
)尺寸模式类似
定义列
使用自动生成的列,可快速创建显示所有数据的DataGrid
控件,但放弃了一些控制能力。例如,不能控制列的顺序、每列的宽度、如何格式化列中的值以及应该放在顶部的标题文本的内容。
更强大的方法是将AutoGenerateColumns
属性设置为false以关闭自动列生成功能。然后可使用希望的设置和指定的顺序,明确地定义希望使用的列。为此,需要使用何时的列对象填充DataGrid.Columns
集合。
目前,
DataGrid
控件支持几种类型的列,通过继承自DataGridColumn
的不同类表示这些列:
- DataGridTextColumn:这种列对于大部分数据类型是标准选择。值被转换为文本,并在
TextBlock
元素中显示。当编辑行时,TextBlock
元素被替换为标准的文本框 - DataGridCheckBoxColumn:这种列显示复选框。为Boolean(或可空Boolean)值自动使用这种列类型。通常,复选框是只读的;但当编辑行时,会变成普通的复选框
- DataGridHyperlinkColumn:这种列显示可单击的链接。如果结合使用WPF中的导航容器,如
Frame
或NavigationWindow
,可允许用户导航到其他URI(通常是外部Web站点) - DataGridComboBox:最初这种列看起来与
DataGridTextColumn
类似,但在编辑模式下这种列会变成下拉的ComboBox
控件。当希望将编辑限制与允许的少部分值时,这种列时很好的选择 - DataGridTemplateColumn:这种列时到目前为止功能最强大的选择。这种列允许显示列值定义数据模板,具有在列表控件中使用模板时所具有的所有灵活性和功能。例如,可使用
DataGridTemplateColumn
显示图像数据或专门的WPF控件(如具有合法值的下拉列表或用于日期值的DataPicker控件)
当定义列时,几乎总是设置三个细节:在列顶部显示的题头文本、列的宽度以及获取数据的绑定
通常使用简单字符串设置 DataGridColumn.Header 属性,但不必限制为普通文本。列题头是内容控件,可为 Header 属性提供任何内容,包括图像或具有元素组合的布局面板。
DataGridColumn.Width 属性支持硬编码的值和几种自动尺寸改变模式,上一节中分析的 DataGrid.ColumnWidth 属性类似。唯一的区别是 DataGridColumn.Width 属性应用于单个列,而DataGrid.ColumnWidth 属性为整个表中所有的列设置默认宽度。当设置 DataGridColumn.Width 属性时,会覆盖 DataGrid.ColumnWidth 属性。
DataGridCheckBoxColumn
与 DataGridTextColumn 一样,Binding 属性提取数据–对于这种情况,是用于设置内部CheckBox 元素的 IsChecked 属性的 true 或 false 值。DataGridCheckBoxColumn 还添加了 Content属性,通过该属性可在复选框旁边显示可选内容。最后,**DataGridCheckBoxColumn 提供了IsThreeState属性,该属性确定复选框是否支持未定状态以及更明显的选中和未选中状态。如果使用DataGridCheckBoxColumn 显示来自可空 Boolean 值的信息,可将 IsThreeState 属性设置为 true。**这样用户可通过单击切换到未定状态(显示为具有轻淡阴影的复选框),以便为绑定的值返回nul。
DataGridHyperlinkCoulmn
通常,DataGridHyperlinkColumn 为导航和显示使用相同的信息。但如果愿意的话,可单独指定这些细节。为此,只需要使用 Binding属性设置URI,并使用可选的 ContentBinding 属性从绑定数据对象的不同属性获取显示的文本。
DataGridComboBoxColumn
DataGridComboBoxColumn虽然最初显示为普通文本,但却提供了简明流畅的编辑体验,允许用户从 ComboBox控件的可用选项列表中选择一项(实际上,用户必须从列表中进行选择,因为 ComboBox不允许直接输入文本)。
DataGridTemplateColumn
DataGridTemplateColumn 使用数据模板,其工作方式和在以前研究的用于列表控件的数据模板特征是相同的。唯一的区别是允许在 DataGridTemplateColumn 中定义两个模板:一个用于数据显示(CellTemplate);另一个用于数据编辑(CellEditingTemplate)
1 | <DataGridTemplateColumn> |
设置列的格式和样式
可使用与设置 TextBlock 元索格式相同的方式设置 DataGridTextureColumn 元素的格式,方法就是设置 Foreground、FontFamily、FontSize、FontStyle 以及 FontWeight 属性。然而,DataGridTextColumn 没有提供 TextBlock的所有属性。例如,如果希望创建显示多行文本的列,将无法设置经常使用的 Wrapping属性。对于这种情况,需要改用 ElementStyle 属性。
本质上,**ElementStyle 属性用于创建应用于 DataGrid 单元格内部的元素的样式。**对于简单的 DataGridTextColumn,该元素是 TextBlock。对于 DataGridCheckBoxColumn,单元格内部的元素是复选框。对于 DataGridTemplateColumn,单元格内部的元素是在数据模板中创建的任何元素。
下面是一个允许在列中对文本进行换行的简单样式:
1 | <DataGridTextColumn Header="Description" Width="400" |
可使用 EditingElementStyle 属性为编辑列时使用的元素提供样式。对于 DataGridTextColumn,编辑元素是 TextBox 控件。
ElementStyle、EditingElementStyle 以及其他列属性提供了设置特定列中所有单元格的格式的方法。然而,在某些情况下,可能希望为每一列中的每个单元格应用格式化设置。完成该工作的最简单方法是为 DataGrid.RowStyle 属性配置样式。DataGrid 控件还提供了少部分用于设置网格其他部分(如列题头和行题头)格式的额外属性。
属性 | 样式的适用范围 |
---|---|
ColumnHeaderStyle | 位于网格顶部的列题头的TextBlock |
RowHeaderStyle | 行题头的TextBlock |
DragIndicatorStyle | 当用户正在将列题头拖动到新位置时用于列题头的TextBlock |
RowStyle | 用于普通行(在列中没有通过列的ElementStyle属性明确定制过的行)的TextBlock |
设置行的格式
通过设置 DataGrid 列对象的属性,可控制如何格式化整个列。但在许多情况下,标识包含特定数据的行更有用。例如,可能希望强调价格较高的产品和到期的装运。可通过处理DataGrid.LoadingRow 事件以编程方式应用此类格式。
对于设置行格式,LoadingRow 事件是个非常强大的工具。它提供了对当前行数据对象的访问,允许开发人员执行简单的范围检查、比较以及更复杂的操作。它还提供了行的 DataGridRow对象,允许开发人员使用不同的颜色或不同的字体设置行的格式。然而,不能只设置行中单个单元格的格式–为达到那样的目的,需要使用 DataGridTemplateColumn 和自定义的值转换器。
当每一行出现在屏幕上时,就会立即为该行引发LoadingRow 事件。这种方法的优点是应用程序永远不必格式化整个网格;相反,只为当前可见的行引发LoadingRow 事件。但也有缺点:当用户在网格中滚动时,会连续引发LoadingRow 事件。因此,在LoadingRow方法中不能放置耗时的代码,除非希望慢慢地滚动。
还有一个考虑事项:项容器再循环。为降低内存开销,当在数据中滚动时,DataGrid 控件为显示新数据而重用相同的 DataGridRow对象(这也是为什么将该事件称为LoadingRow而不是CreatingRow 的原因)。如有不慎,DataGrid 控件能够将数据加载到已经格式化了的 DataGridRow对象中。为了防止发生这种情况,必须明确地将每行恢复到其初始状态。
下面的示例为价格较高的项提供明亮的橙色背景,为正常价格的项提供标准的白色背景:
1 | <DataGrid |
1 | private SolidColorBrush highlightBrush = new SolidColorBrush(Colors.Orange); |
只有当加载了行之后,才会应用在 LoadingRow 事件处理程序中应用的格式。如果编辑行,那么不会触发 [oadingRow 代码(至少在将行滚动出视图,然后在将其滚动回视图之前,不会触发 LoadingRow 代码)。
显示行细节
DataGrid 控件还支持行细节(row details)一块可选的独立显示区域,在行的列值的下面显示。行细节区域添加了无法仅使用列实现的两个特征:
- 能够跨越
DataGrid
控件的整个宽度,并且不会切入到独立的列中,从而提供了更多可供使用的空间 - 可配置行细节区域,从而只为选择的行显示该区域,当不需要时允许用户折叠额外的细节
下例显示了一个使用这两种行为的
DataGrid
控件。行细节区域显示能有换行的产品描述文本,并且只为当前选择的产品显示该描述文本
1 | <DataGrid AutoGenerateColumns="False" ItemsSource="{Binding Products}"> |
可通过设置 DataGrid.RowDetailsVisibilityMode 属性来配置行细节区域的显示行为。默认情况下,该属性设置为 VisibleWhenSelected,这意味着显示所选行的行细节区域。另外,也可将其设置为 Visible,这意味着会同时显示所有行的细节区域。还可将该属性设置为Collapsed,这意味着不会为任何行显示细节区域–至少在使用代码修改 RowDetailsVisibilityMode 属性(例如,当用户选择特定类型的行时)之前是这样的。
冻结列
冻结的列位于 DataGrid 控件的左边,甚至当向石滚动时冻结的列仍然位于左边。下图显示了冻结的 Product列在滚动期间如何保持可见。注意水平滚动条只在可滚动的列下面伸展而不会伸展到冻结列的下面。
对于非常宽的网格,列冻结是非常有用的特性,当希望确保特定信息(如产品名或唯一标识符)总是可见时尤其如此。为使用该特性,将DataGrid.FrozenColumn属性设置为大于0的数。例如,数值1只冻结第一列:
1 | <DataGrid |
冻结的列必须总是位于网格的左侧。如果冻结一列,该列是最左边的列;如果结两列,它们将是左边的前两列,等等。
选择
与普通的列表控件类似,DataGrid控件允许用户选择单个项。当选择一项时,可以响成SelectionChanged 事件。为找到当前选择的数据对象,可使用Selectedltem属性。如果希望用户能够选择多行,将 SelectionMode属性设置为 Extended(唯一的另一个选项是 Single,这也是默认选项)。为了选择多行,用户必须按下Shif或Cl键。可从SelectedItems属性中检索所选项的集合。
可使用 SelectedItem 属性通过代码设置选择的项。如果将选择的项设置为当前视图以外的项,那么接着调用 DataGrid.ScrolltoView()方法是个好主意,这会强制 DataGrid 控件向前或向后滚动,直到指定的项可见。
排序
DataGrid 控件内置了排序功能,只要绑定到实现了 IList 接口的集合(如 List
为执行排序,用户需要单击列题头。单击一次会根据列的数据类型以升序排序(例如,数字从0向上排序,字母按照字母顺序进行排序),再次单击该列会翻转排序顺序。在列题头的右边会显示一个箭头,指示根据列中的值对 DataGrid 进行排序。对于升序排序,箭头指向上方;对于降序排序,箭头指向下方。
通常,DataGrid 排序算法使用在列中显示的绑定数据,这是合理的。然而,**可通过设置列的 SortMemberPath 属性从绑定的数据对象中选择不同属性。**并且如果有一个 DataGridTemplateColumn 列,就需要使用 SortMemberPath 属性,因为没有绑定属性提供绑定的数据。如果不这么做,该列就不支持排序。
还可通过将 CanUserSortColumns属性设置为 false来禁用排序(或通过设置列的 CanUserSort
属性,为特定列禁用排序功能)。
编辑
DataGrid
控件的最方便之处在于支持编辑。当用户双击DataGrid
单元格时,该单元格会切换到编辑模式。但DataGrid
控件以几中方式限制这种编辑功能:
- DataGrid.IsReadOnly:当该属性为true时,用户不能编辑任何内容
- DataGridColumn.IsReadOnly:当该元素为true时,用户不能编辑该列中的任意值
- 只读属性:如果数据对象具有没有属性设置器的属性,DataGrid控件将足够智能,它能够注意到该细节,并且禁用列编辑,就像已将
DataGridColumn.IsReadOnly
属性设置为true一样。类似的,**如果属性不是简单的文本、数字或日期类型,DataGrid控件使其为只读(但可通过切换到DataGridTemplateColumn来不就这种情况)
当单元格切换到编辑模式时发生的变化取决于列的类型。DataGridTextColumn 显示文本框(尽管该文本框的外观是无缝的,填满整个单元格并且没有可见的边框)。DataGridCheckBox 列显示可选中或取消选中的复选框。DataGridTempiateColumn 是到目前为止最有趣的,它允许使用更专业的输入控件替换标准的编辑文本框。
例如,下面的列显示日期。当用户双击以编辑该值时,单元格就会变成具有预先选择的当前值的下拉DatePicker控件:
1 | <DataGridTemplateColumn Header="Date Added"> |
DataGrid 控件自动支持基本验证系统,该系统响应在数据绑定系统中的问题(例如不能将提供的文本转换为合适的数据类型)以及由属性设置器抛出的异常。下面的示例使用自定义的验证规则验证 UnitCost 字段:
1 | <DataGridTextColumn Header="UnitCost" Width="100"> |
为DataGridCell提供的默认ErrorTemplate
模板在非法值的周围显示红色的外边框,与其他输入控件(如文本框)相似。
可通过其他几种方法为DataGrid控件实现验证。一种选择是使用DataGrid控件的编辑事件下表列出了这些编辑事件。列出这些事件的顺序也是在 DataGrid 控件中引发这些事件的顺序,
名称 | 说明 |
---|---|
BeginningEdit | 当单元格正进入编辑模式时发生。可检查当前编辑的列和行,检查单元格的值,并且可使用DataGridBeginningEditEventArgs.Cancel 属性取消操作 |
PreparingCellForEdit | 用于模板列。这时,可为编辑控件执行所有最后的初始化操作。可使用DataGridPreparingCellForEditEventArgs.EditingElement访问CellEditingTemplate 中的元素 |
CellEditEnding | 当单元格正退出编辑模式时发生。DataGridCellEditEndingEventsArgs.EditAction 指示用户是试图接受编辑(例如,通过按下 Enter键或单击另一个单元格),还是取消编辑(通过按下Escape 键)。可检查新数据并设置Cancel属性以回滚修改 |
RowEditEnding | 当用户在编辑完当前行之后导航到新行时发生。与CellEditEnding事件一样,可使用该事件执行验证并取消修改。通常,将在此执行涉及几列的验证–例如,确保某列中的值大于另一列中的值 |
如果需要在某个地方执行特定于页面的验证逻辑(从而不会结合到数据对象中),可编写响应 CellEditEnding 和 RowEditEnding 事件的自定义验证逻辑。在 CellEditEnding 事件处理程序中检查列规则,并在 RowEditEnding 事件中验证整行的一致性。请记住,如果取消编辑,应当为发生的问题提供解释(通常是在页面其他地方的 TextBlock 控件中显示解释)。