Wednesday, May 28, 2008

How to: Take control over the Collection Editor's PropertyGrid

The PropertyGrid control is a very useful tool if you want to update object attributes at run-time in a elegant way. The PropertyGrid provides some useful events to let you know what is going on. The problem occures for the collection properties when the Collection Editor form is shown. This form also provides a PropertyGrid control to edit any item from the current collection but we don't have access to any of known events.

The Collection Editor

The Collection Editor is a special form which is controlled by the PropertyGrid in order to manage the collection properties of the current selected object.
If you want to know exactly when a property value of your current object has changed, the default Collection Editor may become a real pain if your object contains collection properties. This is because the Collection Editor's PropertyGrid events are not exposed.

Getting the full control

In order to be able to customize the Collection Editor, you have to add a reference to System.Data to your project. First of all let's consider the following class for our collection items:

C# .NET

public class MyItem

{

    private string itemAttribute1;

    private string itemAttribute2;

 

    public string ItemAttribute1

    {

        get { return this.itemAttribute1; }

        set { this.itemAttribute1 = value; }

    }

 

    public string ItemAttribute2

    {

        get { return this.itemAttribute2; }

        set { this.itemAttribute2 = value; }

    }

 

    public MyItem(string attr1, string attr2)

    {

        this.itemAttribute1 = attr1;

        this.itemAttribute2 = attr2;

    }

}


Now we create a class for our main object that will contains the items collection and will be exposed to the PropertyGrid. Please notice the attribute I used to mark the List property to call our customized Collection Editor instead the default one.

C# .NET

public class MyObject

{

    private string objectAttribute;

    private List<MyItem> myCollection;

 

    public string ObjectAttribute

    {

        get { return this.objectAttribute; }

        set { this.objectAttribute = value; }

    }

 

    // This attribute will enable our customized collection

    // editor for this property...

    [Editor(typeof(MyCollectionEditor), typeof(UITypeEditor))]

    public List<MyItem> MyCollection

    {

        get { return this.myCollection; }

    }

 

    public MyObject(string attr)

    {

        this.objectAttribute = attr;

        this.myCollection = new List<MyItem>();

    }

}


The customized Collection Editor

The basic idea is to get a reference to the CollectionEditor's default form and find the inner PropertyGrid in its Control collection. Once we have found the inner PropertyGrid we can hook some event handlers to it and expose the corresponding event args using some static events defined for our custom Collection Editor.

C# .NET

public class MyCollectionEditor : CollectionEditor

{

    // Define a static event to expose the inner PropertyGrid's

    // PropertyValueChanged event args...

    public delegate void MyPropertyValueChangedEventHandler(object sender,

                                        PropertyValueChangedEventArgs e);

    public static event MyPropertyValueChangedEventHandler MyPropertyValueChanged;

 

    // Inherit the default constructor from the standard

    // Collection Editor...

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

 

    // Override this method in order to access the containing user controls

    // from the default Collection Editor form or to add new ones...

    protected override CollectionForm CreateCollectionForm()

    {

        // Getting the default layout of the Collection Editor...

        CollectionForm collectionForm = base.CreateCollectionForm();

 

        Form frmCollectionEditorForm = collectionForm as Form;

        TableLayoutPanel tlpLayout = frmCollectionEditorForm.Controls[0] as TableLayoutPanel;

 

        if (tlpLayout != null)

        {

            // Get a reference to the inner PropertyGrid and hook

            // an event handler to it.

            if (tlpLayout.Controls[5] is PropertyGrid)

            {

                PropertyGrid propertyGrid = tlpLayout.Controls[5] as PropertyGrid;

                propertyGrid.PropertyValueChanged += new PropertyValueChangedEventHandler(propertyGrid_PropertyValueChanged);

            }

        }

 

        return collectionForm;

    }

 

    void propertyGrid_PropertyValueChanged(object sender, PropertyValueChangedEventArgs e)

    {

        // Fire our customized collection event...

        if (MyCollectionEditor.MyPropertyValueChanged != null)

        {

            MyCollectionEditor.MyPropertyValueChanged(this, e);

        }

    }

}


How to use the static events

Here is a short sample of how the custom static events should be used:

C# .NET

public Form1()

{

    InitializeComponent();

 

    MyCollectionEditor.MyPropertyValueChanged += new MyCollectionEditor.MyPropertyValueChangedEventHandler (MyCollectionEditor_MyPropertyValueChanged);

 

    MyObject obj = new MyObject("Object 1");

    obj.MyCollection.Add(new MyItem("Test1", "Item1"));

    obj.MyCollection.Add(new MyItem("Test2", "Item2"));

 

    this.propertyGrid1.SelectedObject = obj;

}

 

void MyCollectionEditor_MyPropertyValueChanged(object sender, PropertyValueChangedEventArgs e)

{

    // Now you know when a collection item has been updated!

}


If you want to know how to set a PropertyGrid as read-only, please read this article: How to: Set the PropertyGrid as Read-Only

kick it on DotNetKicks.com

15 comments:

Anonymous said...

Great and exclusive solution!
It is what I looked for!

Anonymous said...

Great, thank you

Anonymous said...

Excellent!!! its very simple, yet very obscure to find.
The last pice a was missing.
Thank you... thank you.
FDiderot

Unknown said...
This comment has been removed by the author.
Anonymous said...

Error 1 The type or namespace name 'CollectionEditor' could not be found (are you missing a using directive or an assembly reference?) E:\Projects\TestProjects\TestProperty\TestProperty\MyCollectionEditor.cs 8 39 TestProperty


Not compiling in .Net 2.0

Anonymous said...

You need the References System.Design to access the CollectionEditor
and
using System.ComponentModel.Design;

Anonymous said...

does this includes when a new item is added?

jaschwa said...

Thanks. This gave me the clues I needed to handle the destruction of an item by overriding CanRemoveInstance() and DestroyInstance().

Unknown said...

This works great for property changes...but it doesn't capture the Add and Delete Click events.

Ali B said...

Awesome. Now I get PropertyValueChanged event triggered when I change a property inside an item inside a collection. However, no event is triggered when items are added or removed. So I've used your same skeleton above but changed the event to FormClosed. This way I get notified when the user closes the collection editor.

Post here: http://alibad.wordpress.com/2010/01/12/propertgrid-collection-events/

Anonymous said...

thanks, great article!

for all .net4 users: you need to add a reference to system.design which is not available in the ".net4 client profile". you have to switch to ".net4 framework". (in project properties)

Anonymous said...

Here is a way less awkward workaround for this problem that simulates a change by invoking the MemberwiseClone function of System.Object:

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 (modified && value != null)
{
value = value.GetType()
.GetMethod("MemberwiseClone", BindingFlags.Instance | BindingFlags.NonPublic)
.Invoke(value, new object[] { });
}
return value;
}

protected override CollectionForm CreateCollectionForm()
{
CollectionForm collectionForm = base.CreateCollectionForm();
TableLayoutPanel tlpLayout = collectionForm.Controls[0] as TableLayoutPanel;

foreach(Control table in collectionForm.Controls)
{
if(!(table is TableLayoutPanel)) { continue; }
foreach(Control grid in table.Controls)
{
if(!(grid is PropertyGrid)) { continue; }
((PropertyGrid)grid).PropertyValueChanged +=new PropertyValueChangedEventHandler(FixedCollectionEditor_PropertyValueChanged);
}

}
return collectionForm;
}

void FixedCollectionEditor_PropertyValueChanged(object s, PropertyValueChangedEventArgs e)
{
modified = true;
}
}

Anonymous said...

there is an "error" in the example above .. remove the following line:

TableLayoutPanel tlpLayout = collectionForm.Controls[0] as TableLayoutPanel;

Rick said...

Hi - I've tried this (and this solution has been found elsewhere across the net) but the problem is, like you, my property doesn't have a setter -- it's read only. What this means is if I specify a custom editor for this property (which is of type List) then it's greyed out on my property grid. If I do not specify a custom editor, it's not greyed out, but then I cannot implement your solution. It seems very strange that it's greyed out (almost seems like a bug) when a custom editor is specified. Any thoughts?

Luis Miguel Romero said...

We need add an Assembly reference System.Design. Project->Add reference.