Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Looking for work-around for inability of DataGridView control to bind to hierarchical (OO) data

It would seem that the DataGridView control can only bind to data sources that are flat (all the Properties are primative types). My data is hierarchal. For example:

    interface INestedObj
{
    string Prop3 { get; }
}

interface IParentObj
{
    public string Prop1 { get; }
    public string Prop2 { get; }
    public INestedObj NestedObj { get; }
}

Given this, how does one bind to an object implementing IParentObj? Eventually you are faced with having to do something like this:

grid.Columns["prop1Col"].DataPropertyName = "Prop1";
grid.Columns["prop2Col"].DataPropertyName = "Prop2";

grid.Columns["prop3Col"].DataPropertyName = "How to display Prop3?";

grid.Columns["prop3Col"].DataPropertyName = "NestedObj.Prop3"; // does not work

I am looking for advice and/or work-arounds.

TIA

like image 737
Michael J Avatar asked Nov 17 '10 21:11

Michael J


2 Answers

You can expose properties from INestedObj for binding, but the solution is very messy.To give some background, all WinForms controls which support databinding use TypeDescriptor to determine which properties exist on the objects they're binding to. Through TypeDescriptionProvider and CustomTypeDescriptor, you can override the default behaviour and thusly add/hide properties - in this case, hiding the NestedObj property and replacing it with all of the properties on the nested type.

The technique i'm going to show has 2 (big-ish) caveats:

  1. Since you're working with interfaces (and not concrete classes), you have to add the custom type descriptor at runtime.
  2. The custom type descriptor needs to be able to create a concrete instance of IParentObj, therefore it must know one such class which has a default constructor.

(Please excuse the lengthy code)

First, you need a way of wrapping a PropertyDescriptor from the nested type so that it can be accessed from the parent type:

public class InnerPropertyDescriptor : PropertyDescriptor {
    private PropertyDescriptor innerDescriptor;

    public InnerPropertyDescriptor(PropertyDescriptor owner, 
        PropertyDescriptor innerDescriptor, Attribute[] attributes)
        : base(owner.Name + "." + innerDescriptor.Name, attributes) {
        this.innerDescriptor = innerDescriptor;
    }
    public override bool CanResetValue(object component) {
        return innerDescriptor.CanResetValue(((IParentObj)component).NestedObj);
    }
    public override Type ComponentType {
        get { return innerDescriptor.ComponentType; }
    }
    public override object GetValue(object component) {
        return innerDescriptor.GetValue(((IParentObj)component).NestedObj);
    }
    public override bool IsReadOnly {
        get { return innerDescriptor.IsReadOnly; }
    }
    public override Type PropertyType {
        get { return innerDescriptor.PropertyType; }
    }
    public override void ResetValue(object component) {
        innerDescriptor.ResetValue(((IParentObj)component).NestedObj);
    }
    public override void SetValue(object component, object value) {
        innerDescriptor.SetValue(((IParentObj)component).NestedObj, value);
    }
    public override bool ShouldSerializeValue(object component) {
        return innerDescriptor.ShouldSerializeValue(
            ((IParentObj)component).NestedObj
        );
    }
}

Then you need to write a custom type descriptor that exposes the properties from the nested type:

public class ParentObjDescriptor : CustomTypeDescriptor {
    public override PropertyDescriptorCollection GetProperties(
        Attribute[] attributes) {
        PropertyDescriptorCollection properties
            = new PropertyDescriptorCollection(null);

        foreach (PropertyDescriptor outer in TypeDescriptor.GetProperties(
            new ParentObj() /* concrete implementation of IParentObj */, 
            attributes, true)) {
            if (outer.PropertyType == typeof(INestedObj)) {
                foreach (PropertyDescriptor inner in TypeDescriptor.GetProperties(
                    typeof(INestedObj))) {
                    properties.Add(new InnerPropertyDescriptor(outer, 
                        inner, attributes));
                }
            }
            else {
                properties.Add(outer);
            }
        }

        return properties;
    }
}

...and then you need a way of exposing the descriptor from above:

public class ParentObjDescriptionProvider : TypeDescriptionProvider {
    public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, 
        object instance) {
        return new ParentObjDescriptor();
    }
}

Finally, at run-time (before you bind to the DataGridView), you must associate the type description provider with the IParentObj interface. You can't do this at compile-time because TypeDescriptionProviderAttribute can't be placed on interfaces...

TypeDescriptor.AddProvider(new ParentObjDescriptionProvider(), typeof(IParentObj));

I tested this by binding a DataGridView to an IParentObj[] and, low and behold, it creates columns for Prop1, Prop2 and NestedObj.Prop3.

You have to ask yourself, though... is it really worth all that effort?

like image 172
Bradley Smith Avatar answered Nov 15 '22 08:11

Bradley Smith


Here is a simple solution that came to me at the end of a long day.

I used a Linq query and projection to create an anonymous type that displays the proper information in the DataGridView.

var query = from pt in parentObjCollection
  select new {Prop1=pt.Prop1, Prop2=pt.Prop2, NestedObj.Prop3=pt.NestedObj.Prop3};

I had to supply the proper value (NestedObj.Prop3) to the DataPropertyName property to get the value to display in the grid.

When I have more time I am going to try and implement Bradley's solution.

like image 25
Michael J Avatar answered Nov 15 '22 08:11

Michael J