I am posting this with as much explanation as I can since I had a hard time finding generic information on this subject and wanted to share my findings with the SO community.
Data binding to a collection of complex objects in C# does not normally allow for reading data from the nested object(s) within the class. An example of this is a member of an instance of class A
is an object of class B
. If you need properties from the inner object (B
in this case) when a collection / binding source is used as a Data Source you are out of luck without additional work or access to the original class for modification.
The question is "How can you use data from inner classes when databinding to a UI object, without access to modify the original class?"
The data in the inner classes can absolutely be used in data-binding mapping, but not by default. The best way to handle this is by setting up PropertyDescriptors
and TypeDescriptors
. The way I am going to explain below is in a mostly generic implementation, but will allow for data-binding access to the inner objects without requiring any modification of the original classes or extensions to implement interfaces. This is great if you are not the author of the classes you are using or if your are using ORM mapped classes.
There are 4 parts to implement this solution:
PropertyDescriptor
class to access the inner objectsCustomTypeDescriptor
implementationTypeDescriptonProvider
implementationPART 1 - Extending the PropertyDescriptor
class:
In order to access the inner components we need to get their PropertyDescriptor
s, which are essentially metadata used to access the class's public properties. This can be done by extending PropertyDescriptor
to access child properties. Additionally, this where you implement how to read and write back to these objects, or set them to read only (as I did).
class SubPropertyDescriptor : PropertyDescriptor
{
private PropertyDescriptor _parent;
private PropertyDescriptor _child;
public SubPropertyDescriptor(PropertyDescriptor parent, PropertyDescriptor child, string propertyDescriptorName)
: base(propertyDescriptorName, null)
{
_child = child;
_parent = parent;
}
//in this example I have made this read-only, but you can set this to false to allow two-way data-binding
public override bool IsReadOnly{ get { return true; } }
public override void ResetValue(object component) { }
public override bool CanResetValue(object component){ return false; }
public override bool ShouldSerializeValue(object component){ return true;}
public override Type ComponentType{ get { return _parent.ComponentType; } }
public override Type PropertyType{ get { return _child.PropertyType; } }
//this is how the value for the property 'described' is accessed
public override object GetValue(object component)
{
return _child.GetValue(_parent.GetValue(component));
}
/*My example has the read-only value set to true, so a full implementation of the SetValue() function is not necessary.
However, for two-day binding this must be fully implemented similar to the above method. */
public override void SetValue(object component, object value)
{
//READ ONLY
/*Example: _child.SetValue(_parent.GetValue(component), value);
Add any event fires or other additional functions here to handle a data update*/
}
}
Part 2 - Implementing a CustomTypeDescriptor
:
The CustomTypeDesciptor
is what creates the metadata tags to allow the binding of data from the inner objects. Essentially, we will be creating 'descriptor strings' that link to the Type's properties for the inner objects and then adding them onto the parent object. The format used for the inner objects will be the following "className_property"
where classname is the Type
of the inner object.
class MyClassTypeDescriptors : CustomTypeDescriptor
{
Type typeProp;
public MyClassTypeDescriptors(ICustomTypeDescriptor parent, Type type)
: base(parent)
{
typeProp = type;
}
//This method will add the additional properties to the object.
//It helps to think of the various PropertyDescriptors are columns in a database table
public override PropertyDescriptorCollection GetProperties(Attribute[] attributes)
{
PropertyDescriptorCollection cols = base.GetProperties(attributes);
string propName = ""; //empty string to be populated later
//find the matching property in the type being called.
foreach (PropertyDescriptor col in cols)
{
if (col.PropertyType.Name == typeProp.Name)
propName = col.Name;
}
PropertyDescriptor pd = cols[propName];
PropertyDescriptorCollection children = pd.GetChildProperties(); //expand the child object
PropertyDescriptor[] propDescripts = new PropertyDescriptor[cols.Count + children.Count];
int count = cols.Count; //start adding at the last index of the array
cols.CopyTo(propDescripts, 0);
//creation of the 'descriptor strings'
foreach (PropertyDescriptor cpd in children)
{
propDescripts[count] = new SubPropertyDescriptor(pd, cpd, pd.Name + "_" + cpd.Name);
count++;
}
PropertyDescriptorCollection newCols = new PropertyDescriptorCollection(propDescripts);
return newCols;
}
}
At this point we now have our 'descriptor strings' for setting the bindings to the innre objects. The inner properties of MyClass
can be called like "MyOtherClass_Property1"
and the other properties can called like usual with their variable names "Property1"
Part 3 - Implementing a TypeDescriptonProvider
:
This is the last custom piece that we will need to create. A TypeDescriptionProvider
is the piece that the data-bound object will use to determine what the properties of an object are and is what is used to actually call our CustomTypeDescriptor
class whenever the descriptors are needed. This is also the one class that utilizes generics, but is not actually a generic class since we must connect it to our outer object (aka the data-type of the collection being used).
class MyClassTypeDescProvider<T> : TypeDescriptionProvider
{
private ICustomTypeDescriptor td;
public DigiRecordBindingTypeDescProvider()
: this(TypeDescriptor.GetProvider(typeof(MyClass)))
{ }
public MyClassTypeDescProvider(TypeDescriptionProvider parent)
: base(parent)
{ }
public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance)
{
if (td == null)
{
td = base.GetTypeDescriptor(objectType, instance);
td = new MyClassTypeDescriptors(td, typeof(T));
}
return td;
}
}
The generic class 'T' is used to designate the Type
of the inner object property that we will need to link to our parent object. You will see how this works in the next step.
Part 4 - Attaching our Provider to the Parent Type:
Now that we have created the infrastructure to access the data stored in inner properties we must tell the system to use our customized provider when looking up our TypeDescriptors
. This is done using the static method:
TypeDescriptor.AddProvider(provider,type)
This should be done for each inner Type
, where we need access to the inner as properties. The adding of the provider should be done BEFORE binding the data to the bound object, such as when setting the DataSource
property of the UI object for example.
IQueryable<MyClass> myData = PopulateCollectionWithData();
TypeDescriptor.AddProvider(new MyClassTypeDescProvider<MyOtherClass>(), typeof(MyClass));
TypeDescriptor.AddProvider(new MyClassTypeDescProvider<MyThirdClass>(), typeof(MyClass));
DataGridView1.DataSource = myData; //don't bind directly to a collection if you are doing two-way binding. Use a BindingSource instead!
Finally, if for some reason you need to remove this provider and revert back to the default you can perform the exact same operation in reverse:
TypeDescriptor.RemoveProvider(new MyClassTypeDescProvider<MyOtherClass>(), typeof(MyClass));
TypeDescriptor.RemoveProvider(new MyClassTypeDescProvider<MyThirdClass>(), typeof(MyClass));
See TypeDescriptor Class - MSDN or The MSDN blog that put me on the right track for more information. Additionally, during my research on this I stumbled upon this SO question, which prompted me to post a full explanation since it was really just asking for part 4 of this answer. I hope this helps someone out so they don't need to dig into the System.ComponentModel
library as much as I unnecessarily had to!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With