views:

142

answers:

2

I love PropertyGrid, well, at least the concept behind it - use reflection and attributes to edit your objects without writing much UI code.

My excitement died out pretty quickly though, the default PropertyGrid shipping with WinForms flat-out sucks. Well, it's fine for editing simple objects and such, but that's as far as it goes.

  • It doesn't display appropriate UITypeEditors for dynamic properties which have type "Object".
  • As soon as your objects contain collections, you might be able to edit them with so called CollectionEditor. However, it won't fire PropertyValueChanged event. So once you need to add undo functionality, you're screwed.
  • And I still haven't found an elegant way to add validation for CollectionEditor.
  • It's also problematic to implement undo if you have multiple objects selected, because in that case PropertyValueChanged event args ChangedItem is null.

I soon found myself writing hacks to address those issues with less than agreeable results.

What would you do? Is there an elegant solution to at least the first three issues? Is there an alternative propertygrid? Preferably free & without PInvokes?

+2  A: 

A lot of the PropertyGrid's elegance comes from its simplicity. Above all else, it's designed to play nice with Visual Studio, and i'd expect to see it used primarily in custom UITypeEditors and extensions, rather than in application code.

Presumably the objects you are attaching to the PropertyGrid are classes of your own design? I've found that, in order to make good use of the property grid, you have to heavily decorate your classes and members with attributes.

You may find some joy in writing your own subclasses of CollectionEditor (and other types of editors) and attaching them to class members using the [Editor] attribute - if you can attach this attribute to your dynamic properties, you can force the use of a particular editor.

The only way I can think of adding validation to CollectionEditor is to override the CreateCollectionForm() method, returning an instance of your own, custom subclass of CollectionEditor.CollectionForm. There's a chance you will be able to fire the change events from here.

Unfortunately all I can do is nod and agree with the assertion about implementing undo. You might have to resort to 'backing up' the affected objects via cloning or serialization in order to implement undo.

I've seen alternatives to the built-in property grid control, but they exist mainly to offer different visual styles.

Bradley Smith
+1  A: 

If someone is interested - here is a workaround for the PropertyValueChanged problem that simulates a change by invoking the MemberwiseClone function of System.Object if the CollectionEditor's PropertyValueChanged had been fired ...

public class FixedCollectionEditor : CollectionEditor
{        
    bool modified;

    public FixedCollectionEditor(Type type)
        : base(type)
    { }

    public override object EditValue(System.ComponentModel.ITypeDescriptorContext context, IServiceProvider provider, object value)
    {            
        value = base.EditValue(context, provider, value);
        if (value != null && modified)
        {
            value = value.GetType()
                .GetMethod("MemberwiseClone", BindingFlags.Instance | BindingFlags.NonPublic)
                .Invoke(value, new object[] { });                
        }
        modified = false;
        return value;
    }

    protected override CollectionForm CreateCollectionForm()
    {
        CollectionForm collectionForm = base.CreateCollectionForm();

        foreach (Control table in collectionForm.Controls)
        {
            if (!(table is TableLayoutPanel)) { continue; }
            foreach (Control c1 in table.Controls)
            {
                if (c1 is PropertyGrid)
                {
                    PropertyGrid propertyGrid = (PropertyGrid)c1;
                    propertyGrid.PropertyValueChanged += new PropertyValueChangedEventHandler(GotModifiedHandler);
                }
                if (c1 is TableLayoutPanel)
                {
                    foreach (Control c2 in c1.Controls)
                    {
                        if (!(c2 is Button)) { continue; }
                        Button button = (Button)c2;
                        if (button.Name == "addButton" || button.Name == "removeButton")
                        {
                            button.Click += new EventHandler(GotModifiedHandler);
                            if (button.ContextMenuStrip != null)
                            {
                                button.ContextMenuStrip.ItemClicked += new ToolStripItemClickedEventHandler(GotModifiedHandler);
                            }
                        }
                    }
                }
            }
        }
        return collectionForm;
    }

    void GotModifiedHandler(object sender, EventArgs e)
    {
        modified = true;
    }
}
Martin