【问题标题】:Format WPF DataGrid on multiple row condition在多行条件下格式化 WPF DataGrid
【发布时间】:2014-12-25 11:55:13
【问题描述】:

我有一个 WPF DataGrid,其中包含这样的数据

Number  | Attribute  | Old     | New         |
=============================================|
1       | Height     | 1.1     | 0.9         |
--------+------------+---------+-------------|
1       | Material   | Steel1  | Steel2      |
--------+------------+---------+-------------|
2       | Color      | Green   | Light-Green |
--------+------------+---------+-------------|

由于前 2 条记录由于相同的 Number 而属于一起,我想删除 2 条记录之间的边界,使其看起来像这样

Number  | Attribute  | Old     | New         |
=============================================|
1       | Height     | 1.1     | 0.9         |
1       | Material   | Steel1  | Steel2      |
--------+------------+---------+-------------|
2       | Color      | Green   | Light-Green |
--------+------------+---------+-------------|

我有一种方法可以在加载时格式化一行

private void myGrid_LoadingRow(object sender, DataGridRowEventArgs e) {
        ...
}

但这只能格式化这一行的数据,我不知道哪一行在之后或之前。所以我无法决定如何格式化这一行的边框。

如何根据当前行以及前后行的信息来设置行的格式?

【问题讨论】:

  • 当使用标准 DataGrid 时,没有像这样将行合并在一起的直接方法。有一些第三方网格可能支持这种事情,但如果你试图一起破解一个解决方案,如果你开始让用户应用排序、分组或过滤,这将成为一场噩梦。
  • @MikeStrobel:但我不想合并行。我只想在之前和之后的记录中格式化它们
  • 它们是否真正“合并”并不是重点。您希望根据相邻行是否共享数据来有条件地格式化行,这将比您想象的要复杂得多。可以根据排序和过滤添加、删除和重新排列行,更不用说为行虚拟化回收了。
  • 我已经编辑了你的标题,因为没有“C# DataGrid”这样的东西。
  • 为什么不按数字分组呢?为您的实体创建自定义集合类型并将它们与自定义数据模板绑定。

标签: c# wpf wpfdatagrid


【解决方案1】:

我编写了一个简单的示例应用程序,其中只有一个 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 来组合显示所需的数据格式信息。

所以,AttributeUpdateAttributeUpdateViewModel 共享相同的数据,但视图模型添加了几个处理格式的属性。

用于格式化的新属性有哪些?

  • IsLastInGroup - 所讨论的行是否是其组的最后一行(其中组中的所有项目共享相同的Number)。
  • BorderThickness - 边界的Thickness。在这种情况下,如果该项目在组中的最后一个,则底部边框为 1,其他所有内容为 0,否则为 0。

数据绑定在 XAML 文件中显示为 {Binding name_of_property},只需在视图模型中挖掘数据和格式信息。如果在您的应用程序运行期间底层数据可能发生变化,您将希望让您的视图模型实现INotifyPropertyChanged interfaceINotifyPropertyChanged 本质上为您的应用程序添加了“更改检测”,允许您的绑定自动重新绑定到新的/更改的数据。

最后一点是我使用了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();

【讨论】:

  • 您正在从 unsorted rawAttributeUpdates 序列中填充 AttributeUpdateViewModels,如果原始列表尚未按数字排序,则会给出错误的结果。复制集合初始化程序中的项目块以了解我的意思。由于此解决方案依赖于按数字对源集合进行排序,因此一旦用户按任何其他列排序,它就会崩溃。
  • @MikeStrobel,谢谢,两个很好的收获。我修改了查询,使数据首先按数字排序,然后根据排序的数据进行分组和选择。我还在答案底部编辑了一个新查询,以解决用户按其他列排序的可能性。
【解决方案2】:

如果您只想隐藏后跟具有相同 ID 的行的行的底部边框,那么为什么不将当前行模型与下一行模型进行比较呢?

private void myGrid_LoadingRow(object sender, DataGridRowEventArgs e)
{
    DataGrid grid = (DataGrid)sender;
    object rowModel = e.Row.Item;
    int index = grid.Items.IndexOf(e.Row.Item);

    bool hideBottomBorder = false;
    if (index + 1 < grid.Items.Count)
    {
        var thisItem = rowModel as TheRowModel;
        var nextItem = grid.Items[index + 1] as TheRowModel;
        if (thisItem.Number == nextItem.Number)
        {
            hideBottomBorder = true;
        }
    }

    if (hideBottomBorder)
    {
        // hide bottom border
    }
    else
    {
        // show bottom border
    }
}

如果集合是固定的(即,未添加或删除单个项目),即使它被重新排序(因为在这种情况下,“LoadingRow”将为每一行再次触发),上述内容应该可以正常工作。如果可以在您的场景中修改个别行呢?

  • 如果修改了“数字”:最好在视图模型的 PropertyChanged 上监听,或者您可以使用 DataGrid 的 CellEditEnding 事件。
  • 如果添加或删除单独的行:在基础集合上监听 CollectionChanged 事件,或使用 DataGrid 的 AddingNewItem / UnloadingRow 事件。

您只需要为与修改的行相邻的行触发重新计算适当的边框。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-05-06
    • 2011-10-08
    • 2013-01-16
    • 1970-01-01
    • 2016-06-14
    • 1970-01-01
    相关资源
    最近更新 更多