Search code examples
c#winformscollectioneditor

Collection Editor data lost at design time


I'm trying to make a WinForms usercontrol with a Collection<T> as a property (Where T stands for some custom classes). I already read a lot about this topic, however I can't let this work properly at design time (at runtime everything works fine). To be more precise: the collection editor shows fine when I click on the "..." button in the property window and I can add and delete items. But when I click the OK button nothing happens and when I reopen the collection editor, all items are lost. When I take a look at the designer file, I see my property is assigned to null, instead of the composed collection. I'll show you the most important code:

UserControl:

[Browsable(true),
 Description("The different steps displayed in the control."),
 DesignerSerializationVisibility(DesignerSerializationVisibility.Content),
 Editor(typeof(CustomCollectionEditor), typeof(UITypeEditor))]
public StepCollection Steps
{
    get
    {
        return wizardSteps;
    }
    set
    {
        wizardSteps = value;
        UpdateView(true);
    }
}

StepCollection class:

public class StepCollection : System.Collections.CollectionBase
{
    public StepCollection() : base() { }
    public void Add(Step item) { List.Add(item); }
    public void Remove(int index) { List.RemoveAt(index); }
    public Step this[int index]
    {
        get { return (Step)List[index]; }
    }
}

Step class:

[ToolboxItem(false),
DesignTimeVisible(false),
Serializable()]
public class Step : Component
{
    public Step(string name) : this(name, null, StepLayout.DEFAULT_LAYOUT){ }
    public Step(string name, Collection<Step> subSteps) : this(name, subSteps, StepLayout.DEFAULT_LAYOUT){ }
    public Step(string name, Collection<Step> subSteps, StepLayout stepLayout)
    {
        this.Name = name;
        this.SubSteps = subSteps;
        this.Layout = stepLayout;
    }
    // In order to provide design-time support, a default constructor without parameters is required:
    public static int NEW_ITEM_ID = 1;
    public Step()
        : this("Step" + NEW_ITEM_ID, null, StepLayout.DEFAULT_LAYOUT)
    {
        NEW_ITEM_ID++;
    }
    // Some more properties
}

CustomCollectionEditor:

class CustomCollectionEditor : CollectionEditor
{
    private ITypeDescriptorContext mContext;

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

    public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
    {
        mContext = context;
        return base.EditValue(context, provider, value);
    }
    protected override object CreateInstance(Type itemType)
    {
        if (itemType == typeof(Step))
        {
            Step s = (Step)base.CreateInstance(itemType);
            s.parentContext = mContext; // Each step needs a reference to its parentContext at design time
            return s;
        }
        return base.CreateInstance(itemType);
    }
}

The things I already tried:

When finishing this post, I just found this topic: Simplest way to edit a collection in DesignMode? It's exactly the same problem I experience, however I can't use the proposed answer because I don't use a standard collection.


Solution

  • The articles mentioned by Reza Aghaei are really interesting. However I think I'm close at a more simple solution to my problems:

    As I already noticed, the EditValue property of the collectionForm stayed null, despite of adding items to the collection. Now, I'm not actually sure what happens inside the EditValue method of the collection editor, but I guess it catches an exception because my initial value of my collection is null (it is not initialized in the constructor) and thus returning null instead of creating a new collection. By making the following changes in my custom collection editor class, I get very promising results:

    public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
    {
        mContext = context;
        if (value == null) value = new Collection<Step>();
        Collection<Step> result = (Collection<Step>)base.EditValue(context, provider, value);
        if (result != null && result.Count == 0) return null;
        return result;
    }
    

    Notice the second line inside the method, which assigns a new Collection to the initial value. By doing this, my collection is persisted and everything works almost fine.

    The only thing I want to fix now is the serialization to the designer file. Currently something like this is produced:

    // wizardStepsControl1
    // ...
    this.wizardStepsControl1.Steps.Add(this.step1);
    // ...
    
    // step1
    // Initialization of step1
    

    This code will give an exception because wizardStepsControl1.Steps is never initialized to a collection. What i would like to be produced is something like this:

    this.wizardStepsControl1.Steps = new Collection<Step>();
    this.wizardStepsControl1.Steps.Add(step1);
    // ...
    

    Even better would be that the whole collection is initialized at first and afterwards assigned to my control's Steps property. I'll see what I can do to let it work and post some updates here, maybe it is required for this to implement an InstanceDescriptor or make my custom Collection class inherit from Component (since components are always initialized in the designer files).

    I know this is a totally different question than my first one, so maybe I'll start a new one for this. But if someone knows the answer already it would be great to hear it here!

    UPDATE: I found a solution to my problem.

    Inheriting from Component and from CollectionBase wasn't possible because it is not allowed by C#. Implementing a TypeConverter which converts my custom collection to an InstanceDescriptor didn't work too (I don't know why, I guess it's because a Collection is serialized in a different way than a normal custom class).

    But by creating a CodeDomSerializer, I was able to add code to the produced designer's code. This way I was able to initialize my collection if some items were added to it during design time:

    public class WizardStepsSerializer : CodeDomSerializer
    {
        /// <summary>
        /// We customize the output from the default serializer here, adding
        /// a comment and an extra line of code.
        /// </summary>
        public override object Serialize(IDesignerSerializationManager manager, object value)
        {
            // first, locate and invoke the default serializer for 
            // the ButtonArray's  base class (UserControl)
            //
            CodeDomSerializer baseSerializer = (CodeDomSerializer)manager.GetSerializer(typeof(WizardStepsControl).BaseType, typeof(CodeDomSerializer));
    
            object codeObject = baseSerializer.Serialize(manager, value);
    
            // now add some custom code
            //
            if (codeObject is CodeStatementCollection)
            {
    
                // add a custom comment to the code.
                //
                CodeStatementCollection statements = (CodeStatementCollection)codeObject;
                statements.Insert(4, new CodeCommentStatement("This is a custom comment added by a custom serializer on " + DateTime.Now.ToLongDateString()));
    
                // call a custom method.
                //
                CodeExpression targetObject = base.SerializeToExpression(manager, value);
                WizardStepsControl wsc = (WizardStepsControl)value;
                if (targetObject != null && wsc.Steps != null)
                {
                    CodePropertyReferenceExpression leftNode = new CodePropertyReferenceExpression(targetObject, "Steps");
                    CodeObjectCreateExpression rightNode = new CodeObjectCreateExpression(typeof(Collection<Step>));
                    CodeAssignStatement initializeStepsStatement = new CodeAssignStatement(leftNode, rightNode);
                    statements.Insert(5, initializeStepsStatement);
                }
    
            }
    
            // finally, return the statements that have been created
            return codeObject;
        }
    }
    

    By relating this serializer to my custom control with the DesignerSerializerAttribute, the following code is produced in the designer file:

    // 
    // wizardStepsControl1
    // 
    // This is a custom comment added by a custom serializer on vrijdag 4 september 2015
    this.wizardStepsControl1.Steps = new System.Collections.ObjectModel.Collection<WizardUserControl.Step>();
    // ...
    this.wizardStepsControl1.Steps.Add(step1);
    // ...
    

    which is exactly what I wanted.

    I took most of this code from https://msdn.microsoft.com/en-us/library/system.componentmodel.design.serialization.codedomserializer(v=vs.110).aspx