Actually I was interested in the same subject, so it was the target of the my first SO question Cross tabular data binding in WPF. The provided answer there covered it in general, but not for the DataGrid
specific binding. So my conclusion was that the solution should be based on some System.ComponentModel
concepts:
(1) Custom PropertyDescriptor implementation for providing "virtual" properties.
(2) The item class implementing ICustomTypeDescriptor to expose the "virtual" properties per item
(3) The collection class implementing ITypedList to allow automatic data grid column creation from the "virtual" properties.
Having your model
class RegionSale
{
public DateTime DateSale;
public string Region;
public double DollarAmount;
}
and data
IEnumerable<RegionSale> data = ...;
the "virtual" properties can be determined at runtime by simply getting a distinct list of the Region
field:
var regions = data.Select(sale => sale.Region).Distinct().ToList();
For each region we'll create a property descriptor with region as Name
, which later will be used as key into item internal dictionary to retrieve the value.
The items will be built by grouping the data by DateSale
.
Here is the whole implementation:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
class RegionSalePivotViewItem : CustomTypeDescriptor
{
private RegionSalePivotView container;
private Dictionary<string, double> amountByRegion;
internal RegionSalePivotViewItem(RegionSalePivotView container, DateTime date, IEnumerable<RegionSale> sales)
{
this.container = container;
DateSale = date;
amountByRegion = sales.ToDictionary(sale => sale.Region, sale => sale.DollarAmount);
}
public DateTime DateSale { get; private set; }
public double? GetAmount(string region)
{
double value;
return amountByRegion.TryGetValue(region, out value) ? value : (double?)null;
}
public override PropertyDescriptorCollection GetProperties()
{
return container.GetItemProperties(null);
}
}
class RegionSalePivotView : ReadOnlyCollection<RegionSalePivotViewItem>, ITypedList
{
private PropertyDescriptorCollection properties;
public RegionSalePivotView(IEnumerable<RegionSale> source) : base(new List<RegionSalePivotViewItem>())
{
// Properties
var propertyList = new List<PropertyDescriptor>();
propertyList.Add(new Property<DateTime>("DateSale", (item, p) => item.DateSale));
foreach (var region in source.Select(sale => sale.Region).Distinct().OrderBy(region => region))
propertyList.Add(new Property<double?>(region, (item, p) => item.GetAmount(p.Name)));
properties = new PropertyDescriptorCollection(propertyList.ToArray());
// Items
((List<RegionSalePivotViewItem>)Items).AddRange(
source.GroupBy(sale => sale.DateSale,
(date, sales) => new RegionSalePivotViewItem(this, date, sales))
.OrderBy(item => item.DateSale)
);
}
public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors) { return properties; }
public string GetListName(PropertyDescriptor[] listAccessors) { return null; }
class Property<T> : PropertyDescriptor
{
Func<RegionSalePivotViewItem, Property<T>, T> getValue;
public Property(string name, Func<RegionSalePivotViewItem, Property<T>, T> getValue) : base(name, null) { this.getValue = getValue; }
public override Type ComponentType { get { return typeof(RegionSalePivotViewItem); } }
public override Type PropertyType { get { return typeof(T); } }
public override object GetValue(object component) { return getValue((RegionSalePivotViewItem)component, this); }
public override bool IsReadOnly { get { return true; } }
public override bool CanResetValue(object component) { return false; }
public override void ResetValue(object component) { throw new NotSupportedException(); }
public override void SetValue(object component, object value) { throw new NotSupportedException(); }
public override bool ShouldSerializeValue(object component) { return false; }
}
}
Sample test:
ViewModel:
class ViewModel
{
public RegionSalePivotView PivotView { get; set; }
}
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:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<DataGrid x:Name="dataGrid" HorizontalAlignment="Left" Margin="28,33,0,0" VerticalAlignment="Top" Height="263" Width="463" ItemsSource="{Binding PivotView}"/>
</Grid>
</Window>
Code behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var data = new[]
{
new RegionSale { DateSale = new DateTime(2015, 12, 03), Region = "UK", DollarAmount = 23634 },
new RegionSale { DateSale = new DateTime(2015, 12, 03), Region = "US", DollarAmount = 22187 },
new RegionSale { DateSale = new DateTime(2015, 12, 04), Region = "UK", DollarAmount = 56000 },
new RegionSale { DateSale = new DateTime(2015, 12, 04), Region = "US", DollarAmount = 22187 },
new RegionSale { DateSale = new DateTime(2015, 12, 14), Region = "UK", DollarAmount = 56000 },
new RegionSale { DateSale = new DateTime(2015, 12, 14), Region = "US", DollarAmount = 10025 },
};
DataContext = new ViewModel { PivotView = new RegionSalePivotView(data) };
}
}
Result:
