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:
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 GridViewColumn
s.
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
.
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:
<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:
class GenericEnumerator : IEnumerator
{
private readonly object[] _list;
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:
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:
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.
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:
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.