Click here to Skip to main content
15,860,859 members
Articles / Desktop Programming / WPF

Binding a ListView to a Data Matrix

Rate me:
Please Sign up or sign in to vote.
5.00/5 (23 votes)
15 May 2009MIT3 min read 160.5K   4K   63   32
Binding a WPF ListView to a DataMatrix with columns determined at runtime

DynamicListView

Introduction

This article mainly shows how to bind a WPF ListView to a DataMatrix (an undefined data source with dynamic columns) where the ListView columns cannot be determined until runtime.

This article assumes that the reader has prior knowledge of WPF Data Binding and Dependency Properties. For more information on these topics, check out the following articles here on the CodeProject.

Background

Most of the work I do consists of churning undefined (but structured) datasets and generating meaningful information and trends - i.e., Data Analysis, Knowledge Discovery, and Trend Analysis. And as such, most of the data I get are never really similar. In situations like these, you need to come up with a generic way of doing basic stuff like data presentations (reporting) for any kind of dataset. This led me to create a DataMatrix a long time ago. But as we all know, it is not very obvious how to handle anonymous data in WPF.

I know I could have just used a regular DataTable and bound it to the WPF DataGrid, but I can assure you, when you deal with millions of records that cannot be virutalized, a DataTable becomes very heavy, hogs memory, and results in too much messy code [behind] with "Magic" strings all over the place. Besides, we have to keep things as M-V-VM as possible.

Using the Code

Basically, what we are going to do is bind a ListView to a DataMatrix class as shown below:

C#
public class DataMatrix : IEnumerable
{
    public List<MatrixColumn> Columns { get; set; }
    public List<object[]> Rows { get; set; } 

    IEnumerator IEnumerable.GetEnumerator()
    {
        return new GenericEnumerator(Rows.ToArray());
    }
}

public class MatrixColumn
{
    public string Name { get; set; }
    public string StringFormat { get; set; }
}

Note that the MatrixColumn can always be extended with all the properties you need to format the GridViewColumns.

To achieve this, we need to create a DependencyProperty which will be added to the ListView so that when the DataMatrixSource is set, we can then bind the undefined columns to the ListView at runtime. Below is the class that contains the DependencyProperty.

C#
public class ListViewExtension
{
    public static readonly DependencyProperty MatrixSourceProperty =
        DependencyProperty.RegisterAttached("MatrixSource",
        typeof(DataMatrix), typeof(ListViewExtension),
            new  FrameworkPropertyMetadata(null,
                new  PropertyChangedCallback(OnMatrixSourceChanged)));

    public static DataMatrix GetMatrixSource(DependencyObject d)
    {
        return (DataMatrix)d.GetValue(MatrixSourceProperty);
    }

    public static void SetMatrixSource(DependencyObject d, DataMatrix value)
    {
        d.SetValue(MatrixSourceProperty, value);
    }

    private static void OnMatrixSourceChanged(DependencyObject d,
        DependencyPropertyChangedEventArgs e)
    {
        ListView listView = d as ListView;
        DataMatrix dataMatrix = e.NewValue as DataMatrix;

        listView.ItemsSource = dataMatrix; 
        GridView gridView = listView.View as GridView; 
        int count = 0; 
        gridView.Columns.Clear();
        foreach (var col in dataMatrix.Columns)
        {
            gridView.Columns.Add(
                new  GridViewColumn
                    {
                        Header = col.Name,
                        DisplayMemberBinding = new Binding(stringM.Format("[{0}]", count))
                    }); 
            count++;
        }
    }
}

This will then be attached to the ListView as shown below:

XML
<ListView cc:ListViewExtension.MatrixSource="{Binding MyDataMatrix}"/>

And that's all. At runtime, your columns will be added dynamically to the ListView's GridView. Note that the ListView's ItemsSource property expects an IEnumerable as the binding element; that is why the matrix implements this interface. This is provided by the GenericEnumerator shown below:

C#
class GenericEnumerator : IEnumerator
{
    private readonly object[] _list;
    // Enumerators are positioned before the first element
    // until the first MoveNext() call. 

    private int _position = -1;

    public GenericEnumerator(object[] list)
    {
        _list = list;
    }

    public bool MoveNext()
    {
        _position++;
        return (_position < _list.Length);
    }

    public void Reset()
    {
        _position = -1;
    }

    public object Current
    {
        get
        {
            try { return _list[_position]; }
            catch (IndexOutOfRangeException) { throw new InvalidOperationException(); }
        }
    }
}

For this example, I used the generic Northwind database and bound to the Orders table (orders count per month/year); then I built a simple DataMatrix from the cross tabulation of the Year (row source) vs. Months (columns source) against the sum for the number of orders.

Given the Orders table, you can generate and bind to a well-known dataset to get the "Number of Orders per Month" as follows:

C#
var orders = from o in db.Orders
 group o by new { o.OrderDate.Value.Year, o.OrderDate.Value.Month }
     into g
     select new MonthlyOrderCount() { Year = g.Key.Year, 
            Month = g.Key.Month, NumberOfOrders = g.Count() };

Which will result as shown below:

Image 2

To get a more meaningful dataset from the above query, we need to cross-tabulate the year vs. month to get the total number of orders. So, we can run the dataset through a reporting algorithm and generate a DataMatrix as follows:

Note: This is a fake DataMatrixGenerator. A real one will determine the columns (at runtime) based on the parameters specified.

C#
private static DataMatrix CreateMatrix(IEnumerable<monthlyordercount> orders)
{
    DataMatrix result = new DataMatrix {Columns = new List<matrixcolumn>(), 
                                        Rows = new List<object[]>()};
    result.Columns.Add(new MatrixColumn() { Name = "Year" });
    for (int i = 0; i < 12; i++)
        result.Columns.Add(new MatrixColumn()
        {
            Name = string.Format("{0:MMM}", new DateTime(2009, i + 1, 1))
        });
    for (int i = 1996; i < 1999; i++)
    {
        object[] row = new object[13];
        row[0] = i;
        for (int j = 1; j <= 12; j++)
        {
            int count = (from o in orders
                         where o.Year == i && o.Month == j
                         select o.NumberOfOrders).Sum();
            row[j] = count;
        }
        result.Rows.Add(row);
    }
    return result;
}

which will result as shown below:

Image 3

We can then use this result set at runtime to bind dynamically to the list view using the DataMatrix.

Points of Interest

This approach can also be used for generating flow documents.

History

  • May 15 2009: Initial release.

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Architect
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 5 Pin
MSdispenser29-Jun-10 2:25
MSdispenser29-Jun-10 2:25 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.