views:

30

answers:

3

All PropertyGrid examples I have seen allow the user to edit a single object, which PropertyGrid scans by reflection. I would like the user to be able to edit, for example, an ini file or a plain-old Dictionary, with one line per key-value pair. Is this possible?

+1  A: 

The PropertyGrid will allow the editing of any property that has get and set accessors or an appropriate editor attached to it or the type it provides that describes how to convert or even edit that value.

Therefore, if you expose a property that is, for example, a Stream of an INI file and attach a custom TypeConverter that expands it to the name/value pairs within it, you can indeed edit an INI file with the PropertyGrid.

Useful links:

Type converters

A type converter is attached to a type using a TypeConverterAttribute declaration. Type converters allow you to provide rules on how to convert your type to and from other types. A range of overrides are provided to tailor your conversions, all beginning with Convert.

Through various GetPropertiesxxxx calls, type converters also allow you to specify what properties your type has that can be edited and how they appear (their names, for example). This makes it possible to expand values (like editing the Point type) and show or hide properties based on the state of a value (for example, your INI file would use this to show or hide made-up properties based on the contents of the file).

Type converters also allow you to specify a list of values that your type can show as a drop-down list when being edited. This is provided by the GetStandardValuesXxx set of overrides and can be useful if you don't want to create a custom editor but you have a fixed list of allowable values.

Editors

Editors allow you to fine tune the design time experience of editing an instance of a type. They can be attached to a property or a type and indicate to the PropertyGrid what editor to use when editing a value. This allows you to show a dialog or your own dropdown with some custom user interface (for example, a slider).

Jeff Yates
I thought PropertyGrid showed one line per property. Would the Stream property itself show up as a heading with a "+" sign that you expand to get the key-value pairs inside it?
Qwertie
@Qwertie: If you create your own type converter and override GetPropertiesSupported to return true, then return some properties, yes, it would be expandable.
Jeff Yates
+1  A: 

Yes. Some years ago, I wrote some code to display an IDictionary in a PropertyGrid.

Roger Lipscombe
This is very useful - but do you know how to assign descriptions to each property, to show in the lower pane of PropertyGrid? I have been poking around in Reflector and the debugger and found no clues about where that is supposed to come from.
Qwertie
Ahh, I found it! Just override MemberDescriptor.Description which is in the base class of PropertyDescriptor: e.g. public override string Description { get { return "Your description here"; } }
Qwertie
@Qwertie: You can use the DescriptionAttribute to add descriptions to properties.
Jeff Yates
@Jeff, there are no actual properties on which to place a DescriptionAttribute.
Qwertie
A: 

Here is a complete example derived from the code that Roger linked to. I updated it so that

  • It uses Dictionary<GridProperty,object> instead of IDictionary
  • Each GridProperty specifies a name, category, description, etc.

(Note, this post has been changed from my original design.) Thanks Roger!

public partial class PropertyEditor : Form
{
    public PropertyEditor()
    {
        InitializeComponent();

        var dict = new Dictionary<GridProperty, object>();
        dict["Food"] = "Poutine";
        dict["Ball"] = "Football";
        dict[new GridProperty("1. Greeting", "Words", "The first word to say")] = "Hello";
        dict[new GridProperty("2. Subject", "Words", "The second word to say")] = "Dogs";
        dict[new GridProperty("3. Verb", "Words", "The third word to say")] = "Like";
        dict[new GridProperty("4. Object", "Words", "The fourth word to say")] = "Burritos";
        dict[new GridProperty("Integer", "Types", "")] = 42;
        dict[new GridProperty("Double", "Types", "")] = 42.5;
        dict[new GridProperty("Color", "Types", "")] = Color.ForestGreen;

        propertyGrid1.SelectedObject = new DictionaryPropertyGridAdapter(dict, "Stuff");
    }
}

/// <summary>
/// Holds information about a property in a Dictionary-based PropertyGrid
/// </summary>
public class GridProperty
{
    public GridProperty(string name)
        { Name = name; }
    public GridProperty(string name, string category)
        { Name = name; Category = category; }
    public GridProperty(string name, string category, string description)
        { Name = name; Category = category; Description = description; }

    public string Name { get; private set; }
    public string Category { get; private set; }
    public string Description { get; set; }
    public bool IsReadOnly { get; set; }
    public object DefaultValue { get; set; } // shown if value is null

    public static implicit operator GridProperty(string name) { return new GridProperty(name); }
}

/// <summary>An object that wraps a dictionary so that it can be used as the
/// SelectedObject property of a standard PropertyGrid control.</summary>
/// <example>
/// propertyGrid.SelectedObject = new DictionaryPropertyGridAdapter(dict, "");
/// </example>
public class DictionaryPropertyGridAdapter : ICustomTypeDescriptor
{
    internal IDictionary<GridProperty, object> _dictionary;
    internal string _defaultCategory;

    public DictionaryPropertyGridAdapter(Dictionary<GridProperty, object> dict, string defaultCategory)
    {
        _dictionary = dict;
        _defaultCategory = defaultCategory;
    }

    public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
    {
        var props = new PropertyDescriptor[_dictionary.Count];
        int i = 0;
        foreach (var prop in _dictionary)
            props[i++] = new GridPropertyDescriptor(prop.Key, this);
        return new PropertyDescriptorCollection(props);
    }

    #region Boilerplate

    #region Never called
    public string GetComponentName()
    {
        return TypeDescriptor.GetComponentName(this, true);
    }
    public EventDescriptor GetDefaultEvent()
    {
        return TypeDescriptor.GetDefaultEvent(this, true);
    }
    EventDescriptorCollection System.ComponentModel.ICustomTypeDescriptor.GetEvents()
    {
        return TypeDescriptor.GetEvents(this, true);
    }
    PropertyDescriptorCollection System.ComponentModel.ICustomTypeDescriptor.GetProperties()
    {
        return GetProperties(null);
    }
    #endregion

    public string GetClassName()
    {
        return TypeDescriptor.GetClassName(this, true);
    }
    public EventDescriptorCollection GetEvents(Attribute[] attributes)
    {
        return TypeDescriptor.GetEvents(this, attributes, true);
    }
    public TypeConverter GetConverter()
    {
        return TypeDescriptor.GetConverter(this, true);
    }
    public object GetPropertyOwner(PropertyDescriptor pd)
    {
        return _dictionary;
    }
    public AttributeCollection GetAttributes()
    {
        return TypeDescriptor.GetAttributes(this, true);
    }
    public object GetEditor(Type editorBaseType)
    {
        return TypeDescriptor.GetEditor(this, editorBaseType, true);
    }
    public PropertyDescriptor GetDefaultProperty()
    {
        return null;
    }

    #endregion

    class GridPropertyDescriptor : PropertyDescriptor
    {
        GridProperty _prop;
        DictionaryPropertyGridAdapter _parent;

        internal GridPropertyDescriptor(GridProperty prop, DictionaryPropertyGridAdapter parent)
            : base(prop.Name, null)
        {
            _prop = prop;
            _parent = parent;
        }
        public override string Description
        {
            get { return _prop.Description; }
        }
        public override string Category
        {
            get { return _prop.Category ?? _parent._defaultCategory; }
        }
        public override Type PropertyType
        {
            get { return (_parent._dictionary[_prop] ?? _prop.DefaultValue ?? "").GetType(); }
        }
        public override void SetValue(object component, object value)
        {
            _parent._dictionary[_prop] = value;
        }
        public override object GetValue(object component)
        {
            return _parent._dictionary[_prop];
        }
        public override bool IsReadOnly
        {
            get { return _prop.IsReadOnly; }
        }
        public override Type ComponentType
        {
            get { return null; }
        }
        public override bool CanResetValue(object component)
        {
            return _prop.DefaultValue != null;
        }
        public override void ResetValue(object component)
        {
            SetValue(component, _prop.DefaultValue);
        }
        public override bool ShouldSerializeValue(object component)
        {
            return false;
        }
    }
}
Qwertie