I have a WPF DataGrid with data like this
Number | Attribute | Old | New |
=============================================|
1 | Height | 1.1 | 0.9 |
--------+------------+---------+-------------|
1 | Material | Steel1 | Steel2 |
--------+------------+---------+-------------|
2 | Color | Green | Light-Green |
--------+------------+---------+-------------|
Since the first 2 records belong together due to the same Number
I would like to remove the border between the 2 records so it will look like this
Number | Attribute | Old | New |
=============================================|
1 | Height | 1.1 | 0.9 |
1 | Material | Steel1 | Steel2 |
--------+------------+---------+-------------|
2 | Color | Green | Light-Green |
--------+------------+---------+-------------|
I have a method to format a row on loading
private void myGrid_LoadingRow(object sender, DataGridRowEventArgs e) {
...
}
But this can only format on data of this very row and I do not know which row comes after or before. So I can't decide how to format the border of this row.
How can I format the row depending on information of not just the current row but previous and following rows?
I've written a simple sample app that has just a XAML file and code-behind. To recreate what I've done, simply create a new WPF 4.5 application, and paste the code below into the proper files.
My solution uses view models, which allow you to do everything with data bindings (and do not require you to wire up events in the code-behind).
This may seem like a lot more code than you were anticipating, but keep in mind that this is a full example, and a lot of it is just setup. For the code that really matters, hopefully you will find that, even though it adds up to a decent number of lines, it gives you a very powerful template for creating all sorts of cool user interfaces in WPF. I've added some commentary after each code file to hopefully make it a little easier to figure out what the code does.
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>
This is basically just a simple data grid with text columns. The magic is the custom row style, which creates horizontal grid lines as needed. (See below for more details on the data bindings.)
MainWindow.xaml.cs (i.e., the code-behind):
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(); }
}
}
}
Basically, I've assumed that the data you are showing in each row of your table is an AttributeUpdate
. (I just made that up, you probably have a better name.)
Since an AttributeUpdate
is pure data and has nothing to do with how your data should be formatted, I created an AttributeUpdateViewModel
to combine the data and formatting information needed for display purposes.
So, AttributeUpdate
and AttributeUpdateViewModel
share the same data, but the view model adds a couple properties that deal with formatting.
What are the new properties used for formatting?
IsLastInGroup
- Whether the row in in question is the last of its group (where all items in the group share the same Number
).BorderThickness
- The Thickness
of the border. In this case, if the item is last in the group, 1 for the bottom border and zero for everything else, otherwise, 0 all around.The data bindings, which appear as {Binding name_of_property}
in the XAML file, simply tap into the data and formatting information in the view models. If the underlying data can change during the time your application is running, you will want to have your view models implement the INotifyPropertyChanged interface. INotifyPropertyChanged
essentially adds "change detection" to your application, allowing your bindings to automatically re-bind to the new/changed data.
A final point is that I used a LINQ query to take care of the grouping logic. This particular query sorts the rows by Number
, then groups them by Number
. Then it creates AttributeUpdateViewModel
instances, filling in IsLastInGroup
based on whether the current AttributeUpdate
matches the last item in its group.
NOTE: To keep things simple, I put several classes in one file. The usual convention is one class per file, so you may want to break out each class into its own file.
The result
Edit
@Mike Strobel's comment makes the point that sorting by Number may not necessarily be desirable. For example, the user may wish to sort by a different column, but still see rows grouped by Number. I'm not sure this would be a common use case, but if this is a requirement, you could simply substitute in a different LINQ query that compares "current" values with "next" values, then determines whether the Number
changes. Here is my crack at it:
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();