Tuesday 17 August 2010

Modifying the C# CollectionEditor for real-time updates

In one of my projects I'm using a PropertyGrid element in the main form as an easy way for users to change settings. One item in the class used by the PropertyGrid is a collection and C# automatically opens a CollectionEditor form for the user to add, remove and modify elements of the collection.

The issue however is that changes to the collection should be represented in the main area of the form immediately, but the CollectionEditor control seems to leave the collection in an undefined state until the form is closed.

I can understand what the CollectionEditor form is trying to do, it is waiting until the user clicks on OK or Cancel before writing the changes to the actual collection. However it doesn't do this, it partly mucks up the collection when the user changes items or adds them, but doesn't touch the actual collection when the user removes or rearranges items.

I wish to change this behaviour, so that all changes are reflected in the actual collection immediately. To avoid any confusion for the user I also want to remove the Cancel button from the form.

First we need to derive our own class using CollectionEditor as the base class, and then override the CreateCollectionForm method:
public class MyCollectionEditor : CollectionEditor
    {
        protected override CollectionForm CreateCollectionForm()
        {
            // Getting the default layout of the Collection Editor...
            collectionForm = base.CreateCollectionForm();

            Form frmCollectionEditorForm = collectionForm as Form;

            Button cancel = frmCollectionEditorForm.CancelButton as Button;
            
            cancel.Visible = false;
            cancel.Enabled = false;

            frmCollectionEditorForm.CancelButton = null;

            return collectionForm;
        }
    }
As you can see here it simply calls the base constructor to create the default CollectionEditor form, then gets the cancel button of the form and sets it to disabled and not visible. This is the first part done.

Next we need to tell the PropertyGrid to use our custom editor and not the default CollectionEditor. This is easy to do using the Editor attribute right before the collection item in the class used by the PropertyGrid:
[Editor(typeof(MyCollectionEditor), typeof(System.Drawing.Design.UITypeEditor))]
public List<mylistitemtype> ListItems { get { return listItems; } }
Now when the user clicks the "..." in the PropertyGrid control for this item, our MyCollectionEditor class will be used for the editor form.

The next step is to get notified when the user makes any changes to the order of the collection, add or removes items, or changes any of the items. We have to go back to our implementation of CreateCollectionForm() for this, and find the add/remove buttons, the PropertyGrid control and the ListBox control. We can then add event handlers to the OnClick or OnChange event of each of these and take action. This code goes just before the return statement above:
if (frmCollectionEditorForm.Controls[0] is TableLayoutPanel)
{
    TableLayoutPanel tlp = frmCollectionEditorForm.Controls[0] as TableLayoutPanel;

    // Hook up to the PropertyGrid
    if (tlp.Controls[5] is PropertyGrid)
        (tlp.Controls[5] as PropertyGrid).PropertyValueChanged += MyCollectionEditor_PropertyValueChanged;

    if (tlp.Controls[1] is TableLayoutPanel)
    {
        TableLayoutPanel tlp2 = tlp.Controls[1] as TableLayoutPanel;
                    
        // Find the Add button
        if( tlp2.Controls[0] is Button )
            (tlp2.Controls[0] as Button).Click += MyCollectionEditor_AddClick;

        // And the Remove button
        if (tlp2.Controls[1] is Button)
            (tlp2.Controls[1] as Button).Click += MyCollectionEditor_RemoveClick;
    }
                
    // Hook into the list box
    if (tlp.Controls[4] is ListBox)
    {
        listBox = tlp.Controls[4] as ListBox;
        listBox.SelectedIndexChanged += listBox_SelectedIndexChanged;
    }
}

frmCollectionEditorForm.FormClosing += frmCollectionEditorForm_FormClosing;
frmCollectionEditorForm.Load += frmCollectionEditorForm_Load;
As you can see we are relying on the specific implementation of the base class for picking out the various items, obviously if this is changed in future we will need to change this code. Note that we also add event handlers for when the form is opened or closed, as we probably want to be notified of that in our code too.

The final step is how to get the "live" version of the collection, as the form will not update the actual collection correctly until the form is closed. For this we can use the Value property of every items from the ListBox (which we found in the code above). The only slight issue is that we cannot access the Value property so have to use reflection to get at the actual data. The event handler code would then include something like this:
ListBox.ObjectCollection items = listBox.Items;
List<mylistitemtype> newList = new List<mylistitemtype>();                

foreach( Object o in items )
{
    PropertyInfo p = o.GetType().GetProperty("Value");
    if (p.GetValue(o, null) is MyListItemType)
        newList.Add(p.GetValue(o, null) as MyListItemType);
}
So using the above methods we will be notified whenever the user makes any changes to the CollectionEditor, and also we will have the list exactly as the user sees it in the form. We can use this to update the GUI in real time to any changes.

4 comments:

  1. Excellent, helped me tons on my current project, thanks for posting it.

    ReplyDelete
  2. Same here, learned a lot. Thanks.

    ReplyDelete
  3. this is very good post and i learnt all i required.

    ReplyDelete
  4. This article is still helping people in 2020! Thanks for the info.

    ReplyDelete