Tuesday, October 4, 2011
Complex Data Binding a collection implementing IBindingList and ITypedList
Introduction
Data binding can be nasty - especially when you cant use theSystem.Data
objects. I have tried to use all the features of data binding to show them to you. It is pretty small, but takes a hell of a long way to get there.The sample shows a custom collection with objects in it having properties, some of those properties are collections again, with objects in them - i.e. hierarchical data. Maybe some of the properties can't be edited directly because the grid is not smart enough - like
SqlTypes
.The Problem
We have a collection of objects that need to go into a WindowsDataGrid
. They may or may not have properties that are collections too. The Solution
We need to create the following objects:- A class implementing
ITypedList
andIBindingList
(your collection) - A class that is your data (lots of properties and stuff)
- A class implementing
IComparer
(for sorting) - A class implementing
PropertyDescriptor
(for using non grid compatible types, likeSqlTypes
) - A class implementing
Attribute
(so we know what's in the collection)
ITypedList.GetItemProperties(PropertyDescriptor[] listAccessors)
This was the nastiest method interface in the project. The problem had to do with the listAccessors
. If the parameter is not null, then the only one that should be processed is the last one. I had to decompile System.Data.DataView.ITypedList.GetItemProperties
to find that out. Then I needed to detect our custom collections and return the properties for the underlying data.SqlPropertyDescriptor : PropertyDescriptor
This one, although straight forward to implement, is a bit tricky in that when calling GetItemProperties
and we want to use this PropertyDesciptor
instead of the default, we need to replace it in the collection. I wrote this method to do it: Collapse | Copy Code
protected PropertyDescriptorCollection
GetPropertyDescriptorCollection( ArrayList properties )
{
if ( properties == null || properties.Count == 0 )
return new PropertyDescriptorCollection(null);
ArrayList output = new ArrayList();
foreach ( PropertyDescriptor p in properties )
{
if ( p.Attributes.Matches(new Attribute[]
{new BindableAttribute(false)}) ) continue;
if ( p.PropertyType.Namespace == "System.Data.SqlTypes" )
{
// create the base type property descriptor
output.Add(SqlPropertyDescriptor.
GetProperty( p.Name, p.PropertyType ) );
}
else
{
output.Add(p);
}
}
return new PropertyDescriptorCollection((PropertyDescriptor[])
output.ToArray(typeof(PropertyDescriptor)));
}
After that, it was all straight forward.The
SetValue
and GetValue
of the PropertyDescriptor
provide the means to edit SqlTypes
like SqlDateTime
. After detecting the namespace of the data type, I use SqlPropertDescriptor
which implements GetValue()
and SetValue()
. These methods try to figure everything out for themselves:public override void SetValue(object component,object value)
{
try
{
PropertyInfo pi = component.GetType().GetProperty(this.Name);
Object o;
if ( value == DBNull.Value )
{
o = component.GetType().GetField("Null",
BindingFlags.Static | BindingFlags.Public |
BindingFlags.GetField).GetValue(component);
}
else
{
o = pi.PropertyType.GetConstructor(new Type[]
{BaseType}).Invoke(new Object[]{value});
}
pi.SetValue(component,o, null);
}
catch(Exception ex)
{
Debug.WriteLine(ex);
}
}
public override object GetValue(object component)
{
try
{
object Property = component.GetType().GetProperty
(this.Name).GetValue(component,null);
// handle nulls
if ( (bool)Property.GetType().GetProperty
("IsNull").GetValue(Property,null) )
return DBNull.Value;
object Value = Property.GetType().GetProperty
("Value").GetValue(Property,null);
return Value;
}
catch(Exception ex)
{
Debug.WriteLine(ex);
}
return null;
}