2

The items source of my data grid is a collection of objects like:

Public Property Quarter As Integer
Public Property MyColumns() As New List(Of MyColumn)

Now I want to have a grid binding in a way that my resulting grid looks like

-- Quarter -- Column1 -- Column2 -- Column3 .... ColumnX

All items in datasource will have same MyColumns.

Is there a way by which I can bind a collection to grid columns ?

Eugene Podskal
  • 10,270
  • 5
  • 31
  • 53
Muds
  • 4,006
  • 5
  • 31
  • 53
  • http://stackoverflow.com/questions/1983033/how-do-i-dynamically-generate-columns-in-a-wpf-datagrid may give you a few ideas - basically wrap you property+collection objects into `ExpandoObject`s with property+(property for each item in collection) and apply the solution proposed there. – Eugene Podskal Mar 20 '15 at 19:19
  • thanls for coming back Eugene, but to be honest I have used expandoObj before, infact never heard :( .. am trying it out but its getting messier as of now ... – Muds Mar 23 '15 at 16:39
  • It turns out that my first comment was a bit misleading(it is possible to do it so, but it will be far too complex). The most simple and direct solution is described here - http://stackoverflow.com/questions/24361013/wpf-dictionaryint-liststring-in-datagrid. It uses manual column generation and [binding to indexers](https://msdn.microsoft.com/en-us/library/ms752300.aspx#Path%5FSyntax). – Eugene Podskal Mar 23 '15 at 18:35
  • I have added the solution as an answer. First part deals with arbitrary length collection, the second deals exactly with your case. – Eugene Podskal Mar 23 '15 at 21:22
  • I also got the ExpandoObject thing working :) .... thanks for the tip learnt a new feature .. and its pretty exciting... you might wanna post that as second answer as that might be useful to the lot – Muds Mar 24 '15 at 08:58
  • and you know what, for autobinding, I needed property name as A B with a space in between .. n expando lets you have property names with spaces in them.. haha m going crazy on expando :P – Muds Mar 24 '15 at 10:11
  • If you want you can post your own solution with `ExpandoObject` - I'll gladly upvote it. Just do not forget that `dynamic` is ["contagious"](http://stackoverflow.com/questions/25665111/when-passing-dynamic-to-method-the-result-is-dynamic-expression-even-if-it-is). Basically, as it is with every other tool, `dynamic` is the right tool for the right job. Used in a right place it can greatly simplify the development, while used in a wrong place it will deprive you of all the advantages conferred by the strong static typing (Intellisense, compile checks...). – Eugene Podskal Mar 24 '15 at 12:06
  • o yea I understand that.. :) thanks for warning though !.... I will post my ans in a bit thanks – Muds Mar 24 '15 at 12:09
  • posted please suggest improvements if you can spot any thanks – Muds Mar 24 '15 at 12:40

2 Answers2

2

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);
    }
Community
  • 1
  • 1
Eugene Podskal
  • 10,270
  • 5
  • 31
  • 53
1

With insight given to me by Eugene on dynamics and expandoObject, this is what I implemented for solving the current problem.

here is my solution --

We have on collection coming from model, and its structure was fairly rigid. So to bind it to a UI Grid we needed another object that maps the current list in a visually different way.

--using Expando Objects--

You can create a collection of ExpandoObjects and map its dynamic properties to the properties of you collection.

Dim pivotItems As Object = New ExpandoObject()
    ' set properties for object'
     pivotItems.Quarter = developmentQuarters.Key
     pivotItems.Name = developmentQuarters.Key.Name

     ' since expando obj is a dictionary for prop name and its value we can set property names dynamically like this'
     For Each developmentModel As DevelopmentModel In developmentModels
         Dim pivotAsDict As IDictionary(Of String, Object) = pivotItems
         pivotAsDict.Add(developmentModel.BusinessGroupName + " " + developmentModel.Currency.Code, developmentModel.DevelopmentPercentage)
      Next
      ReModelledItems.Add(pivotItems)

So now we have a nested object flattened out as a simple collection which has dynamic columns/properties generated based on values in the initial collection.

We can now simply bind this collection of ExpandoObjects

Muds
  • 4,006
  • 5
  • 31
  • 53