The Equivalent to AutoGenerateColumns for a ListView/GridView
In WPF there is no built-in way to make a ListView display generic content. The DataGrid has this ability but is probably too much for just a display.
To this end I have created an AttachedProperty that will allow the user to use a ListView with that ability. To use this AttachedProperty simply use the following XAML.
<ListView ItemsSource="{Binding theList}" Grid.Row="2"
local:DynamicBindingListView.GenerateColumnsGridView="True"
local:DynamicBindingListView.DateFormatString="MM/dd/yyyy">
<ListView.View>
<GridView></GridView>
</ListView.View>
</ListView>
There are actually two AttachedProperties within the DynamicBindingListView. The first property GenerateColumnsGridView is the main property. Setting this property will cause the DynamicBindingListView to monitor changes to the ItemsSource of the ListView. When the ItemsSource is updated the AttachedProperty will scan the items (actually only the first item) of the ItemsSource and using Reflection get the properties of the Objects within the ItemsSource. If there is more than one object type within the collection it will only deal with the properties from the first object.
The second Attached Property is the DateFormatString. This allows the user to define the format for any and all dates displayed by using this AttachedProperty. If it is not defined for the ListView the default display for a date will be used. This property is actually there as a sample of how the user can define the Format of columns but as I said above there is no way (at present) to format different columns of the same type with different formats.
The code for the DynamicBindingListView object is as follows: (VB.NET C# to follow later)
VB.NET Version
Imports System.ComponentModel
Imports System.Collections.Specialized
Imports System.Reflection
Public Class DynamicBindingListView
Public Shared Function GetGenerateColumnsGridView(ByVal element As DependencyObject) As Boolean
If element Is Nothing Then
Throw New ArgumentNullException("element")
End If
Return element.GetValue(GenerateColumnsGridViewProperty)
End Function
Public Shared Sub SetGenerateColumnsGridView(ByVal element As DependencyObject, ByVal value As Boolean)
If element Is Nothing Then
Throw New ArgumentNullException("element")
End If
element.SetValue(GenerateColumnsGridViewProperty, value)
End Sub
Public Shared ReadOnly GenerateColumnsGridViewProperty As _
DependencyProperty = DependencyProperty.RegisterAttached("GenerateColumnsGridView", _
GetType(Boolean?), GetType(DynamicBindingListView), _
New FrameworkPropertyMetadata(Nothing, AddressOf thePropChanged))
Public Shared Function GetDateFormatString(ByVal element As DependencyObject) As String
If element Is Nothing Then
Throw New ArgumentNullException("element")
End If
Return element.GetValue(DateFormatStringProperty)
End Function
Public Shared Sub SetDateFormatString(ByVal element As DependencyObject, ByVal value As String)
If element Is Nothing Then
Throw New ArgumentNullException("element")
End If
element.SetValue(DateFormatStringProperty, value)
End Sub
Public Shared ReadOnly DateFormatStringProperty As _
DependencyProperty = DependencyProperty.RegisterAttached("DateFormatString", _
GetType(String), GetType(DynamicBindingListView), _
New FrameworkPropertyMetadata(Nothing))
Public Shared Sub thePropChanged(ByVal obj As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
Dim lv As ListView = CType(obj, ListView)
Dim descriptor As DependencyPropertyDescriptor = DependencyPropertyDescriptor.FromProperty(ListView.ItemsSourceProperty, GetType(ListView))
descriptor.AddValueChanged(lv, New EventHandler(AddressOf ItemsSourceChanged))
End Sub
Private Shared Sub ItemsSourceChanged(sender As Object, e As EventArgs)
Dim lv As ListView = CType(sender, ListView)
Dim its As IEnumerable = lv.ItemsSource
Dim itsEnumerator As IEnumerator = its.GetEnumerator
Dim hasItems As Boolean = itsEnumerator.MoveNext()
If hasItems Then
SetUpTheColumns(lv, itsEnumerator.Current)
End If
End Sub
Private Shared Sub SetUpTheColumns(theListView As ListView, firstObject As Object)
Dim theClassProperties As PropertyInfo() = firstObject.GetType().GetProperties()
Dim gv As GridView = theListView.View
For Each pi As PropertyInfo In theClassProperties
Dim columnName As String = pi.Name
Dim grv As New GridViewColumn With {.Header = columnName}
If pi.PropertyType Is GetType(DateTime) Then
Dim bnd As New Binding(columnName)
Dim formatString As String = theListView.GetValue(DateFormatStringProperty)
If formatString <> String.Empty Then
bnd.StringFormat = formatString
End If
BindingOperations.SetBinding(grv, TextBlock.TextProperty, bnd)
grv.DisplayMemberBinding = bnd
Else
Dim bnd As New Binding(columnName)
BindingOperations.SetBinding(grv, TextBlock.TextProperty, bnd)
grv.DisplayMemberBinding = bnd
End If
gv.Columns.Add(grv)
Next
End Sub
End Class
C# Version
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Collections.Specialized;
using System.Reflection;
using System.Windows.Controls;
using System.Windows.Threading;
using System.Windows;
using System.Collections;
using System.Windows.Data;
namespace CSharpVersioon
{
public class DynamicBindingListView
{
public static bool GetGenerateColumnsGridView(DependencyObject element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (bool)element.GetValue(GenerateColumnsGridViewProperty);
}
public static void SetGenerateColumnsGridView(DependencyObject element, bool value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(GenerateColumnsGridViewProperty, value);
}
public static readonly DependencyProperty GenerateColumnsGridViewProperty = DependencyProperty.RegisterAttached("GenerateColumnsGridView", typeof(bool?), typeof(DynamicBindingListView), new FrameworkPropertyMetadata(null, thePropChanged));
public static string GetDateFormatString(DependencyObject element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (string)element.GetValue(DateFormatStringProperty);
}
public static void SetDateFormatString(DependencyObject element, string value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(DateFormatStringProperty, value);
}
public static readonly DependencyProperty DateFormatStringProperty = DependencyProperty.RegisterAttached("DateFormatString", typeof(string), typeof(DynamicBindingListView), new FrameworkPropertyMetadata(null));
public static void thePropChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
ListView lv = (ListView)obj;
DependencyPropertyDescriptor descriptor = DependencyPropertyDescriptor.FromProperty(ListView.ItemsSourceProperty, typeof(ListView));
descriptor.AddValueChanged(lv, new EventHandler(ItemsSourceChanged));
}
private static void ItemsSourceChanged(object sender, EventArgs e)
{
ListView lv = (ListView)sender;
IEnumerable its = lv.ItemsSource;
IEnumerator itsEnumerator = its.GetEnumerator();
bool hasItems = itsEnumerator.MoveNext();
if (hasItems)
{
SetUpTheColumns(lv, itsEnumerator.Current);
}
}
private static void SetUpTheColumns(ListView theListView, object firstObject)
{
PropertyInfo[] theClassProperties = firstObject.GetType().GetProperties();
GridView gv = (GridView)theListView.View;
foreach (PropertyInfo pi in theClassProperties)
{
string columnName = pi.Name;
GridViewColumn grv = new GridViewColumn { Header = columnName };
if (object.ReferenceEquals(pi.PropertyType, typeof(DateTime)))
{
Binding bnd = new Binding(columnName);
string formatString = (string)theListView.GetValue(DateFormatStringProperty);
if (formatString != string.Empty)
{
bnd.StringFormat = formatString;
}
BindingOperations.SetBinding(grv, TextBlock.TextProperty, bnd);
grv.DisplayMemberBinding = bnd;
}
else
{
Binding bnd = new Binding(columnName);
BindingOperations.SetBinding(grv, TextBlock.TextProperty, bnd);
grv.DisplayMemberBinding = bnd;
}
gv.Columns.Add(grv);
}
}
}
}
How does it work?
The DynamicBindigListView class holds two AttachedProperties. The first GenerateColumnsGridView is a boolean but in reality it does not use the value provided in XAML. This property is used as an initialization of the class and has a method thePropChanged as a method of setting up for generating columns in a ListView/GridView. The thePropChanged method simply gets a DependencyPropertyDescriptor for the ItemsSource property of the ListView and registers a callback method for when the property is changed (ItemsSourceChanged).
Then when the ItemsSource of the ListView is changed and there are items in the collection it will get the first items and iterate thru the properties. For each property a GridViewColumn is created with a Header equivalent to the property name. When testing this code I used only the DisplayMemberBinding but while that works it would not allow either converters or FormatStrings to be used, so the BindingOperations.SetBinding was added to allow that. I could not find any documentation of why this is so but I went on observed behaviour for this part of the code.
To illustrate how to extend this I added the ability to add a FormatString if the datatype of the property is a DateTime. The DateFormatString attached property is use to pass this information to the class. Adding other attached properties could be used to pass other information for use in binding.
I hope this will help anyone who wants to have the same functionality as the DataGrid with respect to automatically generating columns.