WPF DataGrid 控件中的动态列(第 1 部分)






4.97/5 (12投票s)
本文介绍了如何在 WPF Datagrid 中动态插入和删除列。
引言
Datagrid 控件非常适合显示存储在表中的数据。数据库表中的一行等于 Datagrid 中的一行。当数据存储在多个表中,例如表 A 和表 B,并且表 A 中的行与表 B 中的行存在一对多(也称为 1:N、父子或主从)关系时,表 A 中的行可以引用表 B 中的多行。这种类型的数据可以通过主从数据视图类型显示。另一种数据关系类型是多对多(也称为 N:M 关系)。表 A 的行可以有多个对表 B 的行的引用。但除了前一种情况,表 B 的行也可以被表 A 的多行引用。
本文介绍了一种在 WPF datagrid 控件中显示和修改多对多关系的方法。通过编辑表 A 和/或表 B 的行,可以添加、删除和修改行和列。
本文分为两部分。在第一部分中,我将重点放在处理动态列的解决方案上。为了简化解决方案,我打破了一个架构约束,即顶层对象不应在下层使用(在本例中,网格列是 GUI 层的一部分,而不是视图模型层)。本文的第二部分解决了这个约束。
Using the Code
应用程序
示例代码实现了一个用户管理表单,其中可以管理用户、角色和用户角色分配。角色和用户显示在两个数据网格中。用户角色分配在用户数据网格中完成。因此,该网格具有动态内容,将每个角色显示为单独的复选框列。用户角色分配通过选中相应的复选框来完成。

数据模型
此示例的数据模型由 User 表、Role 表以及用于关联这两个表的 UserRole 表组成。UserRole 表中的一个条目意味着该用户(由其用户 ID 引用)已分配了一个角色(由该角色的 ID 引用)。如果某个用户角色组合没有条目,则意味着该用户没有分配相应的角色。

数据模型是使用 .NET DataSet 实现的。它是一个具有参照完整性的良好内存数据库,并且包含内置通知委托,用于发布数据行的插入、删除和修改。其内容可以存储到 XML 文件中,在本示例中用作持久化机制。
组件和类图
下一个组件图显示了应用程序的分层
- 应用程序:包含 GUI 元素
- ViewModel:包含业务逻辑
- DataModel:包含数据定义和持久性

Application
- MainWindow:GUI 定义,用 XAML 编写
- DataGridColumnsBehavior:一个附加行为,允许修改附加的- DataGrid控件的列。
- UserRoleValueConverter:值转换器实现,定义了当用户选中或取消选中复选框时会发生什么
ViewModel
- MainViewModel:包含视图的显示数据表属性以及动态列处理的数据逻辑
- ColumnTag:用于将对象标记到派生自- DependencyObject的实例(在本例中为- DataGridColumn)的附加属性
数据模型
- DatabaseContext:包含- UserRoleDataSet的单例实例
- UserRoleDataSet:基于- DataSet的数据库实现
实现
数据绑定
应用程序是使用 MVVM 设计模式编写的。这意味着主窗口绑定到主视图模型,视图控件绑定到主视图模型的属性。
| 参考 | 视图控件属性 | ViewModel 属性 | 
| 1 | MainWindow:DataGridRoles.ItemsSource | MainViewModel.Roles | 
|---|---|---|
| 2 | 
 | MainViewModel.Users | 
| 3 | MainWindow:DataGridUsers.Column | MainViewModel.UserRoleColumns | 
广告 1:将数据库角色表绑定到角色数据网格控件
广告 2:将数据库用户表绑定到用户数据网格控件
广告 3:将列的可观察集合绑定到用户网格控件的列属性。通过此属性实现动态列行为,因为视图模型中的逻辑会从该集合中添加和删除列。
数据网格控件的列属性被声明为只读,因此它不能绑定到视图模型属性。DataGridColumnsBehavior 是一个附加行为,克服了这一限制。原始文章和源代码可以在这里找到。
<Window x:Class="Application.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:attachedBehaviors="clr-namespace:Application.AttachedBehaviors"
        xmlns:viewModel="clr-namespace:ViewModel;assembly=ViewModel"
        Title="User Administration" Height="350" Width="525">
    <Window.DataContext>
        <viewModel:MainViewModel/>
    </Window.DataContext>
    <DockPanel LastChildFill="True">
        <ToolBar DockPanel.Dock="Top">
            <Button Content="Save" Command="{Binding SaveCommand}"/>
        </ToolBar>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="146*"/>
                <RowDefinition Height="147*"/>
            </Grid.RowDefinitions>
            <GroupBox x:Name="UsersGroupBox"
                      Grid.Column="0"
                      Header="User Role Assignment">
                <DataGrid x:Name="DataGridUsers"
                          ItemsSource="{Binding Users}"
                          attachedBehaviors:DataGridColumnsBehavior.BindableColumns=
                              "{Binding UserRoleColumns}"
                          AutoGenerateColumns="False"
                          EnableRowVirtualization="False"/>
            </GroupBox>
            <GroupBox x:Name="RolesGroupBox"
                      Grid.Row="1" Grid.Column="0"
                      Header="Roles">
                <DataGrid x:Name="DataGridRoles"
                          ItemsSource="{Binding Roles}"
                          AutoGenerateColumns="False">
                    <DataGrid.Columns>
                        <DataGridTextColumn Header="Name"
                                            Binding="{Binding Name}"/>
                    </DataGrid.Columns>
                </DataGrid>
            </GroupBox>
        </Grid>
    </DockPanel>
</Window>
数据处理
数据保存在 UserRoleDataSet 中的三个表(Role、User 和 UserRole)中。Role 和 User 表通过 DataView 绑定到数据网格控件。DataView 允许修改、插入和删除行以及阻止这些操作。还可以在 DataView 上设置过滤和排序。数据网格控件可以使用 DataView 处理数据操作。可以在数据网格控件中插入、修改和删除行(网格底部有一个新项目行,按下删除键时会删除行),并且数据表通过 DataView 直接更新。
public class MainViewModel
{
    public MainViewModel()
    {
        --- Code omitted ---
        this.UserRoleColumns = new ObservableCollection<DataGridColumn>();
        --- Code omitted ---
    }
    public DataView Users
    {
        get
        {
            return this.dataContext.DataSet.User.DefaultView;
        }
    }
    public DataView Roles
    {
        get
        {
            return this.dataContext.DataSet.Role.DefaultView;
        }
    }
    public ObservableCollection<DataGridColumn> UserRoleColumns { get; private set; }
}
DataSet 可以与数据库连接一起使用,从 SQL 服务器等存储和检索数据。在此应用程序中,我使用持久化机制将数据存储到 XML 文件并从中检索数据。
每个 DataSet 表都有一组事件,可用于获取数据修改通知。当角色表被修改时,该机制用于添加、删除和更新动态列。
public class MainViewModel
{
    public MainViewModel()
    {
        --- Code omitted ---
        this.dataContext = DatabaseContext.Instance;
        this.dataContext.DataSet.Role.RoleRowChanged += this.RoleOnRowChanged;
        this.dataContext.DataSet.Role.RoleRowDeleted += this.RoleOnRoleRowDeleted;
        --- Code omitted ---
    }
    private void RoleOnRowChanged(object sender,
                                  UserRoleDataSet.RoleRowChangeEvent roleRowChangeEvent)
    {
        switch (roleRowChangeEvent.Action)
        {
            case DataRowAction.Change:
                this.UpdateRoleColumn(roleRowChangeEvent.Row);
                break;
            case DataRowAction.Add:
                this.AddRoleColumn(roleRowChangeEvent.Row);
                break;
        }
    }
    private void RoleOnRoleRowDeleted(object sender,
                                      UserRoleDataSet.RoleRowChangeEvent roleRowChangeEvent)
    {
        if (roleRowChangeEvent.Action == DataRowAction.Delete)
        {
            this.DeleteRoleColumn(roleRowChangeEvent.Row);
        }
    }
}
业务逻辑
默认列定义
用户数据网格列定义存储在 UserRolesColumns 集合中。这意味着默认列,即用户的名字和姓氏,也必须在此集合中。为名字和姓氏实例化了两个 DataGridTextColumns,并且单元格内容通过绑定到行的相应字段来绑定到数据行。
public class MainViewModel
{
    public MainViewModel()
    {
        this.GenerateDefaultColumns();
        --- Code omitted ---
    }
    private void GenerateDefaultColumns()
    {
        this.UserRoleColumns.Add(new DataGridTextColumn
        {
            Header = "First Name", Binding = new Binding("FirstName")
        });
        this.UserRoleColumns.Add(new DataGridTextColumn
        {
            Header = "Last Name", Binding = new Binding("LastName")
        });
    }
}
动态列定义
动态列处理分为 3 种操作类型
- AddRoleColumn:当向- Role表中添加角色时调用。它实例化一个新的- DataGridCheckBoxColumn,并分配- CheckBoxColumnStyle和- UserRoleValueConverter。后者实现了用户角色分配逻辑(见下文)。该列用角色实例进行标记,以便分配逻辑能够工作。列的标题设置为角色名称。
- UpdateRoleColumn:当角色行的内容被修改时调用。该逻辑扫描动态列集合,查找用被修改的角色实例标记的列。一旦找到,列的标题将使用角色名称进行更新。绑定机制会自动更新数据网格中的列标题。
- DeleteRole:当角色从- Role表中删除时调用。该逻辑扫描动态列集合,查找用已删除的角色实例标记的列并将其删除。
public class MainViewModel
{
    private void AddRoleColumn(UserRoleDataSet.RoleRow role)
    {
        var resourceDictionary = ResourceDictionaryResolver.GetResourceDictionary("Styles.xaml");
        var userRoleValueConverter = resourceDictionary["UserRoleValueConverter"] as IValueConverter;
        var checkBoxColumnStyle = resourceDictionary["CheckBoxColumnStyle"] as Style;
        var binding = new Binding
                          {
                              Converter = userRoleValueConverter,
                              RelativeSource =
                                  new RelativeSource(RelativeSourceMode.FindAncestor,
                                                     typeof(DataGridCell), 1),
                              Path = new PropertyPath("."),
                              Mode = BindingMode.TwoWay
                          };
        var dataGridCheckBoxColumn = new DataGridCheckBoxColumn
                                         {
                                             Header = role.Name,
                                             Binding = binding,
                                             IsThreeState = false,
                                             CanUserSort = false,
                                             ElementStyle = checkBoxColumnStyle,
                                         };
        ObjectTag.SetTag(dataGridCheckBoxColumn, role);
        this.UserRoleColumns.Add(dataGridCheckBoxColumn);
    }
    private void UpdateRoleColumn(UserRoleDataSet.RoleRow role)
    {
        if (role != null)
        {
            foreach (var userRoleColumn in this.UserRoleColumns)
            {
                var roleScan = ColumnTag.GetTag(userRoleColumn) as UserRoleDataSet.RoleRow;
                if (roleScan == role)
                {
                    userRoleColumn.Header = role.Name;
                    break;
                }
            }
        }
    }
    private void DeleteRoleColumn(UserRoleDataSet.RoleRow role)
    {
        if (role != null)
        {
            foreach (var userRoleColumn in this.UserRoleColumns)
            {
                var roleScan = ColumnTag.GetTag(userRoleColumn) as UserRoleDataSet.RoleRow;
                if (roleScan == role)
                {
                    this.UserRoleColumns.Remove(userRoleColumn);
                    break;
                }
            }
        }
    }
}
用户角色分配
DataGridCheckBoxColumn 将复选框控件绑定到它所显示行中数据的(可空)布尔属性。在本例中,它将是用户数据行中的一个布尔属性,表示用户到角色的分配。由于 UserTable 定义中没有此类属性,因此必须实现另一种解决方案。不是绑定到复选框控件,而是实例化一个值转换器并将其绑定到将包含 CheckBox 控件的 DataGridCell。上面所示的 AddRoleColumn 方法中的 Binding 定义包含一个对值转换器的赋值。绑定控件的相对源被设置为 DataGridCell,作为 CheckBox 控件的祖先找到(绑定在 CheckBox 级别定义)。
每次 DataGrid 单元格最初被修改或失去焦点时,都会调用值转换器的 Convert 方法。在这两种情况下,都会检索用户和角色角色,并返回转换结果(用户是否已分配角色)。用户行从 DataGridCell 的 DataContext 中获取,其中包含 DataRowView 实例,该实例在其 Row 属性中包含用户行。角色从添加列时分配给列的 ColumnTag 中检索。
当 DataGridCell 处于编辑模式时,会订阅 CheckBox 控件的 Checked 事件,不处于编辑模式时则取消订阅。
public class UserRoleValueConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        bool result = false;
        var dataGridCell = value as DataGridCell;
        if (dataGridCell != null)
        {
            var dataRowView = dataGridCell.DataContext as DataRowView;
            if (dataRowView != null)
            {
                var user = dataRowView.Row as UserRoleDataSet.UserRow;
                var role = ColumnTag.GetTag(dataGridCell.Column) as UserRoleDataSet.RoleRow;
                if (user != null && role != null)
                {
                    var checkBox = dataGridCell.Content as CheckBox;
                    if (checkBox != null)
                    {
                        if (dataGridCell.IsEditing)
                        {
                            checkBox.Checked += this.CheckBoxOnChecked;
                        }
                        else
                        {
                            checkBox.Checked -= this.CheckBoxOnChecked;
                        }
                    }
                    result =
                        DatabaseContext.Instance.DataSet.UserRole.Any(
                            x => x.UserRow == user && x.RoleRow == role);
                }
            }
        }
        return result;
    }
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
每当复选框状态被修改时,都会调用 CheckedBoxOnChecked 方法。该逻辑搜索 CheckBox 的 DataGridCell,并获取属于它的用户和角色实例。它将根据 CheckBox.IsChecked 状态以及 UserRoleRow 是否已存在来添加或删除用户角色条目。
    private void CheckBoxOnChecked(object sender, RoutedEventArgs routedEventArgs)
    {
        var checkBox = sender as CheckBox;
        var dataGridCell = ControlHelper.FindVisualParent<DataGridCell>(checkBox);
        if (dataGridCell != null)
        {
            var dataRowView = dataGridCell.DataContext as DataRowView;
            if (checkBox != null && dataRowView != null)
            {
                var user = dataRowView.Row as UserRoleDataSet.UserRow;
                var role = ObjectTag.GetTag(dataGridCell.Column) as UserRoleDataSet.RoleRow;
                if (user != null && role != null)
                {
                    if (checkBox.IsChecked == true
                        && DatabaseContext.Instance.DataSet.UserRole.Any(
                            x => x.UserRow == user && x.RoleRow == role) == false)
                    {
                        DatabaseContext.Instance.DataSet.UserRole.AddUserRoleRow(user, role);
                    }
                    else
                    {
                        var userRole =
                            DatabaseContext.Instance.DataSet.UserRole.FirstOrDefault(
                                x => x.UserRow == user && x.RoleRow == role);
                        if (userRole != null)
                        {
                            userRole.Delete();
                        }
                    }
                }
            }
        }
    }
}
关注点
复选框列样式处理
作为额外的奖励(并为了避免额外的状态逻辑),我添加了在用户数据网格新项目行中不显示 CheckBox 控件的功能。必须修改 DataGridCheckBoxColumn 样式,并根据 DataGridCell 的内容设置 CheckBox 的 Visibility 标志。如果数据行是新项目行,则它具有 NewItemPlaceHolder。使用转换器来获取此信息,并将其映射到 CheckBox 的 Visibility 标志。此问题的解决方案可以在这里找到。
CheckBox 样式在应用程序层中的 Style.xaml 文件中定义。它被附加到应用程序资源的合并字典中。ViewModel 层中一个名为 ResourceDictionaryResolver 的辅助类遍历合并字典容器中的字典,并搜索具有给定名称的字典(该名称在字典的 Source 属性中)。然后可以通过其键名从资源字典中提取复选框样式。
对象标记
标准 WPF DataColumn 不允许对象标记。对象标记是一种允许将对象标记到控件的功能。这在控件可用但无法使用标准应用程序逻辑访问对象的情况下非常有用。在本示例中,可用控件是 DataGridCell 中的 CheckBox,所需对象是与该列对应的角色。该角色被标记到该列,可以在以后检索。ObjectTag 本身是一个 DependencyProperty,可以附加到任何派生自 DependencyObject 的控件类型。此问题的解决方案可以在这里找到。
数据库保存处理
作为第二个小奖励,我实现了当数据修改时保存数据库的功能。该命令绑定到保存工具栏按钮,并检查数据库上下文中的更改。DataSet 具有内置功能,可以测试其内容的修改。当 DataSet 有更改时,按钮启用,否则禁用。当 Command 的 CanExecuteChanged 被订阅时,它会连接到 CommandManager.RequerySuggested。然后,当应用程序空闲时,通过在主线程上下文中调用 CanExecute 方法,按钮状态会自动检查。
结论
本文展示了 WPF DataGrid 控件动态列处理的实现。这是一个直接的 MVVM 实现,其中动态列处理在视图模型层中完成。该解决方案的缺点是 GUI 组件溢出到 ViewModel 层。在下一篇文章中,我将展示一个实现相同应用程序的解决方案,但业务逻辑和 GUI 控件更严格地分离到各自的层中。


