views:

786

answers:

1

I'm displaying a list of objects in a DataGridView. Everything was working fine. Columns were automagicaly added to the DataGridView based on the properties of the objects.

Now I changed the class I'm displaying in the grid to implement ICustomTypeDescriptor. But now the grid now no longer shows any columns or rows when I set it's DataSource to a list of my custom object.

I'm guessing this has something to do with the fact that with ICustomTypeDescriptor each instance shown in each row of each grid could be returning a different set of properties.

I'm implementing ICustomTypeDescriptor so that I can allow the users to dynamically add custom properties to objects at run time. These custom properties should be visible and editable through the DataGridView.

Why does DataGridView not see my ICustomTypeDescriptor methods? Is there another way that I can dynamically add properties to an object that will be displayed in a DataGridView?

+4  A: 

DataGridView looks at the list version of metadata; the rules for this are... complex:

  1. if the data-source implements IListSource, GetList() is evaluated and used as the data-source (continue at 2)
  2. if the data-source implements ITypedList, GetProperties() is used to obtain metadata (exit)
  3. if a typed (non-object) indexer can be found (i.e. public T this[int index]), then T is used as the source via TypeDescriptor.GetProperties(type):
    1. if a TypeDescriptionProvider is assigned, the this is used for metadata against the type (exit)
    2. otherwise reflection is used for metadata (exit)
  4. if the list is non-empty, the first object is used for metadata via TypeDescriptor.GetProperties(list[0]):
    1. if ICustomTypeDescriptor is implemented, then it is used (exit) [*]
    2. if a TypeDescriptionProvider is assigned, the this is used for metadata against the type (exit) [*]
    3. otherwise reflection is used (exit)
  5. else metadata is unavailable (exit)

([*]=I can't remember which way around these two go...)

If you are using List<T> (or similar), then you hit the "simplest" (IMO) case - #3. If you want to provide custom metadata, then; you best bet is to write a TypeDescriptionProvider and associate it with the type. I can write an example but it'll take a while (on the train, probably)...

Edit: here's an example that uses ITypedList; I'll try to tweak it to use TypeDescriptionProvider instead...

Second edit: a full (yet minimal) example using TypeDescriptionProvider follows; long code warning...

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Forms;
// example
static class Program {
    [STAThread]
    static void Main() { 
        PropertyBag.AddProperty("UserName", typeof(string), new DisplayNameAttribute("User Name"));
        PropertyBag.AddProperty("DateOfBirth", typeof(DateTime), new DisplayNameAttribute("Date of Birth"));
        BindingList<PropertyBag> list = new BindingList<PropertyBag>() {
            new PropertyBag().With("UserName", "Fred").With("DateOfBirth", new DateTime(1998,12,1)),
            new PropertyBag().With("UserName", "William").With("DateOfBirth", new DateTime(1997,4,23))
        };

        Application.Run(new Form {
            Controls = {
                new DataGridView { // prove it works for complex bindings
                    Dock = DockStyle.Fill,
                    DataSource = list,
                    ReadOnly = false, AllowUserToAddRows = true
                }
            },
            DataBindings = {
                {"Text", list, "UserName"} // prove it works for simple bindings
            }
        });
    }
}
// PropertyBag file 1; the core bag
partial class PropertyBag : INotifyPropertyChanged {
    private static PropertyDescriptorCollection props;
    public event PropertyChangedEventHandler PropertyChanged;
    void OnPropertyChanged(string propertyName) {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
    static PropertyBag() {
        props = new PropertyDescriptorCollection(new PropertyDescriptor[0], true);
        // init the provider; I'm avoiding TypeDescriptionProviderAttribute so that we
        // can exploit the default implementation for fun and profit
        TypeDescriptionProvider defaultProvider = TypeDescriptor.GetProvider(typeof(PropertyBag)),
            customProvider = new PropertyBagTypeDescriptionProvider(defaultProvider);
        TypeDescriptor.AddProvider(customProvider, typeof(PropertyBag));
    }
    private static readonly object syncLock = new object();
    public static void AddProperty(string name, Type type, params Attribute[] attributes) {
        lock (syncLock)
        {   // append the new prop, into a *new* collection, so that downstream
            // callers don't have to worry about the complexities
            PropertyDescriptor[] newProps = new PropertyDescriptor[props.Count + 1];
            props.CopyTo(newProps, 0);
            newProps[newProps.Length - 1] = new PropertyBagPropertyDescriptor(name, type, attributes);
            props = new PropertyDescriptorCollection(newProps, true);
        }
    }
    private readonly Dictionary<string, object> values;
    public PropertyBag()
    { // mainly want to enforce that we have a public parameterless ctor
        values = new Dictionary<string, object>();
    }    
    public object this[string key] {
        get {
            if (string.IsNullOrEmpty(key)) throw new ArgumentNullException("key");
            object value;
            values.TryGetValue(key, out value);
            return value;
        }
        set {
            if (string.IsNullOrEmpty(key)) throw new ArgumentNullException("key");
            var prop = props[key];
            if (prop == null) throw new ArgumentException("Invalid property: " + key, "key");
            values[key] = value;
            OnPropertyChanged(key);
        }
    }
    internal void Reset(string key) {
        values.Remove(key);
    }
    internal bool ShouldSerialize(string key) {
        return values.ContainsKey(key);
    }
}

static class PropertyBagExt
{
    // cheeky fluent API to make the example code easier:
    public static PropertyBag With(this PropertyBag obj, string name, object value) {
        obj[name] = value;
        return obj;
    }
}

// PropertyBag file 2: provider / type-descriptor
partial class PropertyBag {
    class PropertyBagTypeDescriptionProvider : TypeDescriptionProvider, ICustomTypeDescriptor {
        readonly ICustomTypeDescriptor defaultDescriptor;
        public PropertyBagTypeDescriptionProvider(TypeDescriptionProvider parent) : base(parent) {
            this.defaultDescriptor = parent.GetTypeDescriptor(typeof(PropertyBag));
        }
        public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance) {
            return this;
        }
        AttributeCollection ICustomTypeDescriptor.GetAttributes() {
            return defaultDescriptor.GetAttributes();
        }
        string ICustomTypeDescriptor.GetClassName() {
            return defaultDescriptor.GetClassName();
        }
        string ICustomTypeDescriptor.GetComponentName() {
            return defaultDescriptor.GetComponentName();
        }
        TypeConverter ICustomTypeDescriptor.GetConverter() {
            return defaultDescriptor.GetConverter();
        }
        EventDescriptor ICustomTypeDescriptor.GetDefaultEvent() {
            return defaultDescriptor.GetDefaultEvent();
        }
        PropertyDescriptor ICustomTypeDescriptor.GetDefaultProperty() {
            return defaultDescriptor.GetDefaultProperty();
        }
        object ICustomTypeDescriptor.GetEditor(Type editorBaseType) {
            return defaultDescriptor.GetEditor(editorBaseType);
        }
        EventDescriptorCollection ICustomTypeDescriptor.GetEvents(Attribute[] attributes) {
            return defaultDescriptor.GetEvents(attributes);
        }
        EventDescriptorCollection ICustomTypeDescriptor.GetEvents() {
            return defaultDescriptor.GetEvents();
        }
        PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties(Attribute[] attributes) {
            return PropertyBag.props; // should really be filtered, but meh!
        }
        PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties() {
            return PropertyBag.props;
        }
        object ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor pd) {
            return defaultDescriptor.GetPropertyOwner(pd);
        }
    }
}
// PropertyBag file 3: property descriptor
partial class PropertyBag {
    class PropertyBagPropertyDescriptor : PropertyDescriptor {
        private readonly Type type;
        public PropertyBagPropertyDescriptor(string name, Type type, Attribute[] attributes)
            : base(name, attributes) {
            this.type = type;
        }
        public override object GetValue(object component) {
            return ((PropertyBag)component)[Name];
        }
        public override void SetValue(object component, object value) {
            ((PropertyBag)component)[Name] = value;
        }
        public override void ResetValue(object component) {
            ((PropertyBag)component).Reset(Name);
        }
        public override bool CanResetValue(object component) {
            return true;
        }
        public override bool ShouldSerializeValue(object component) {
            return ((PropertyBag)component).ShouldSerialize(Name);
        }
        public override Type PropertyType {
            get { return type; }
        }
        public override bool IsReadOnly {
            get { return false; }
        }
        public override Type ComponentType {
            get { return typeof(PropertyBag); }
        }
    }
}
Marc Gravell