Here is the solution. It is not the prettiest or the easiest but it is quite configurable. It is based mostly on the idea from the post WPF: Dictionary<int, List<string>> in DataGrid, just turned into a more generalized version with a bit of Expressions and Reflection.
It will work whether items used as ItemsSource
have the same or different amount of items contained in the respective collection property.
Arbitrary length
Here are the required components:
using System.Reflection;
using Expressions = System.Linq.Expressions;
// See - https://stackoverflow.com/questions/2132791/reflecting-over-all-properties-of-an-interface-including-inherited-ones
public static class ReflectionExtensions
{
public static PropertyInfo GetInterfaceProperty(this Type type, String propName, Type returnType)
{
if (propName == null)
throw new ArgumentNullException("propName");
if (returnType == null)
throw new ArgumentNullException("propType");
return type.GetInterfaces()
.Select(parentInterface =>
parentInterface.GetProperty(propName, returnType))
.Where(prop =>
prop != null)
.Single();
}
}
public static class CollectionPropertyDataGridBindingHelper
{
public static void RemoveAutoGeneratedColumns(this DataGrid dataGrid, String propertyName)
{
if (dataGrid == null)
throw new ArgumentNullException("dataGrid");
if (propertyName == null)
throw new ArgumentNullException("propertyName");
var autogeneratedColumns = dataGrid
.Columns
.OfType<DataGridBoundColumn>()
.Where(col =>
(col.Binding as Binding).Path.Path.Equals(propertyName));
foreach (var autoColumn in autogeneratedColumns)
{
dataGrid.Columns.Remove(autoColumn);
}
}
public static void RegenerateColumns<TItem, TPropertyCollectionItem>(
this DataGrid dataGrid,
Expressions.Expression<Func<TItem, IEnumerable<TPropertyCollectionItem>>> propertyExpression,
IEnumerable<TItem> items)
{
RegenerateColumns<TItem, TPropertyCollectionItem>(dataGrid,
propertyExpression,
items,
(index) =>
String.Format("Column - {0}", index));
}
public static void RegenerateColumns<TItem, TPropertyCollectionItem>(
this DataGrid dataGrid,
Expressions.Expression<Func<TItem, IEnumerable<TPropertyCollectionItem>>> collectionPropertyExpression,
IEnumerable<TItem> items,
Func<Int32, String> formatHeader)
{
if (dataGrid == null)
throw new ArgumentNullException("dataGrid");
if (collectionPropertyExpression == null)
throw new ArgumentNullException("propertyExpression");
if (items == null)
throw new ArgumentNullException("items");
if (formatHeader == null)
throw new ArgumentNullException("formatHeader");
var collectionPropInfo = GetCollectionPropertyInfoFor<TItem, TPropertyCollectionItem>(collectionPropertyExpression);
var propertyName = collectionPropInfo.Name;
var getCount = GetCountGetter<TItem, TPropertyCollectionItem>(
collectionPropertyExpression.Compile(),
collectionPropInfo);
// Remove old autocolumns
dataGrid.RemoveAutoGeneratedColumns(propertyName);
Int32 columnsRequired = items.Select(item => getCount(item)).Max();
// Create new columns
GenerateColumns(dataGrid,
formatHeader,
propertyName,
columnsRequired);
}
private static void GenerateColumns(DataGrid dataGrid,
Func<Int32, String> formatHeader,
String propertyName,
Int32 columnsRequired)
{
for (int columnNumber = 0; columnNumber < columnsRequired; columnNumber++)
{
DataGridTextColumn column = new DataGridTextColumn()
{
Header = formatHeader(columnNumber),
Binding = new Binding(String.Format("{0}[{1}]",
propertyName,
columnNumber))
};
dataGrid.Columns.Add(column);
}
}
private static Func<TItem, Int32> GetCountGetter<TItem, TPropertyCollectionItem>(
Func<TItem, IEnumerable<TPropertyCollectionItem>> getCollection,
PropertyInfo propInfo)
{
if (getCollection == null)
throw new ArgumentNullException("getCollection");
if (propInfo == null)
throw new ArgumentNullException("propInfo");
var collectionType = propInfo.PropertyType;
var countGetter = collectionType.GetInterfaceProperty("Count",
typeof(Int32));
if (countGetter != null)
{
return (item) =>
(Int32)countGetter.GetMethod.Invoke(getCollection(item), null);
}
throw new NotImplementedException("Not implemented: For simple IEnumerables the use of Enumerable.Count() method shall be considered.");
}
private static PropertyInfo GetCollectionPropertyInfoFor<TItem, TPropertyCollectionItem>(
Expressions.Expression<Func<TItem,
IEnumerable<TPropertyCollectionItem>>> propertyExpression)
{
if (propertyExpression == null)
throw new ArgumentNullException("propertyExpression");
var memberExp = propertyExpression.Body as Expressions.MemberExpression;
if (memberExp == null)
throw new ArgumentNullException("propertyExpression");
var propInfo = memberExp.Member as PropertyInfo;
if (propInfo == null)
throw new ArgumentNullException("propertyExpression");
if (!propInfo.DeclaringType.IsAssignableFrom(typeof(TItem)))
throw new ArgumentException("propertyExpression");
return propInfo;
}
}
Here is the XAML:
<DataGrid ItemsSource="{Binding Items}" AutoGenerateColumns="False" Name="dataGrid">
<DataGrid.Columns>
<DataGridTextColumn Header="Quarter" Binding="{Binding Quarter}"/>
</DataGrid.Columns>
</DataGrid>
And this is the code-behind:
using Expressions = System.Linq.Expressions
public class Item
{
public Item(Int32 quarter, Int32 repeatColumns)
{
this.Quarter = quarter;
this.MyColumns = Enumerable
.Range(1, repeatColumns)
.ToList();
}
public Int32 Quarter
{
get; set;
}
public IList<Int32> MyColumns
{
get; set;
}
}
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.Items = GetOriginalItems();
this.DataContext = this;
this.ReinitializeColumns();
}
private void ReinitializeColumns()
{
Expressions.Expression<Func<Item, IEnumerable<Int32>>> exp =
obj =>
obj.MyColumns;
this.dataGrid.RegenerateColumns(exp,
this.Items);
}
public IEnumerable<Item> Items
{
get;
private set;
}
public IEnumerable<Item> GetOriginalItems()
{
return new Item[]
{
new Item(1, 3),
new Item(2, 2),
new Item(3, 5),
new Item(4, 2),
};
}
}
Set length
Here is the code that will create the specified amount of columns(you can put it to some standalone class because it is completely self-contained, or to the same class with arbitrary-length methods(in this case just do not forget to remove the duplicate generate methods)). It is a bit simpler and directly suits your needs:
public static void RegenerateColumns<TItem, TPropertyCollectionItem>(
this DataGrid dataGrid,
String propertyName,
Int32 columnsRequired)
{
dataGrid.RegenerateColumns<TItem, TPropertyCollectionItem>(propertyName,
columnsRequired,
index => String.Format("Column - {0}",
index));
}
public static void RegenerateColumns<TItem, TPropertyCollectionItem>(
this DataGrid dataGrid,
String propertyName,
Int32 columnsRequired,
Func<Int32, String> formatHeader)
{
if (dataGrid == null)
throw new ArgumentNullException("dataGrid");
if (propertyName == null)
throw new ArgumentNullException("propertyName");
if (columnsRequired < 0)
throw new ArgumentOutOfRangeException("columnsRequired");
if (formatHeader == null)
throw new ArgumentNullException("formatHeader");
// Remove old autocolumns
dataGrid.RemoveAutoGeneratedColumns(propertyName);
GenerateColumns(dataGrid,
formatHeader,
propertyName,
columnsRequired);
}
private static void GenerateColumns(DataGrid dataGrid,
Func<Int32, String> formatHeader,
String propertyName,
Int32 columnsRequired)
{
for (int columnNumber = 0; columnNumber < columnsRequired; columnNumber++)
{
DataGridTextColumn column = new DataGridTextColumn()
{
Header = formatHeader(columnNumber),
Binding = new Binding(String.Format("{0}[{1}]",
propertyName,
columnNumber))
};
dataGrid.Columns.Add(column);
}
}
And it is the code-behind that uses it:
public MainWindow()
{
InitializeComponent();
this.Items = GetOriginalItems();
this.DataContext = this;
this.ReinitializeColumns(2);
}
private void ReinitializeColumns(Int32 columnsCount)
{
this.dataGrid.RegenerateColumns<Item, Int32>("MyColumns",
columnsCount);
}