Design time serialization of custom type property

369 views Asked by At

So basically I have a custom UserControl containing a private array of Label objects and I want to be able to access exclusively their Text properties from the outside.

I therefore added a property which type LabelTextCollection is an implementation of IEnumerable and has my Label array as its inner list. Furthermore, I added an implementation of UITypeEditor to allow editing from the windows forms designer.

To try it out, I added my control in a form and edited the property's value. All of that works fine until I close and reopen the designer and the labels take back their default values.

After looking around it seems I have to add an implementation of CodeDomSerializer to allow my type to succesfully serialize into the {Form}.Designer.cs file at design time. I tried serializing a comment line first to test it out but no code is generated.

My final goal would be to have a line like

this.{controlName}.Titles.FromArray(new string[] { "Whatever" } )

added at design time after the property was modified using my editor. What am I misunderstanding and/or doing wrong ?

Custom Type

[DesignerSerializer(typeof(LabelTextCollectionSerializer), typeof(CodeDomSerializer))]
public class LabelTextCollection : IEnumerable<string>, IEnumerable
{
    private Label[] labels;

    public LabelTextCollection(Label[] labels)
    {
        this.labels = labels;
    }

    public void SetLabels(Label[] labels)
    {
        this.labels = labels;
    }

    public IEnumerator<string> GetEnumerator()
    {
        return new LabelTextEnum(labels);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return new LabelTextEnum(labels);
    }

    public string this[int index]
    {
        get { return labels[index].Text; }
        set { labels[index].Text = value; }
    }

    public override string ToString()
    {
        if (labels.Length == 0) return string.Empty;
        else
        {
            StringBuilder sb = new StringBuilder("{ ");
            foreach (string label in this)
            {
                sb.Append(label);
                if (label == this.Last()) sb.Append(" }");
                else sb.Append(", ");
            }
            return sb.ToString();
        }
    }

    public string[] ToArray()
    {
        string[] arr = new string[labels.Length];
        for (int i = 0; i < labels.Length; i++) arr[i] = labels[i].Text;
        return arr;
    }

    public void FromArray(string[] arr)
    {
        for(int i = 0; i < arr.Length; i++)
        {
            if (i >= labels.Length) break;
            else labels[i].Text = arr[i];
        }
    }

    public class LabelTextEnum : IEnumerator<string>, IEnumerator
    {
        private readonly Label[] labels;
        private int position = -1;

        public LabelTextEnum(Label[] labels)
        {
            this.labels = labels;
        }

        public object Current
        {
            get
            {
                try
                {
                    return labels[position].Text;
                }
                catch (IndexOutOfRangeException)
                {
                    throw new InvalidOperationException();
                }
            }
        }

        string IEnumerator<string>.Current { get { return (string)Current; } }

        public void Dispose()
        {
            return;
        }

        public bool MoveNext()
        {
            return ++position < labels.Length;
        }

        public void Reset()
        {
            position = -1;
        }
    }
}

Type Editor

public class LabelTextCollectionEditor : UITypeEditor
{
    IWindowsFormsEditorService _service;
    IComponentChangeService _changeService;

    public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
    {
        if (provider != null)
        {
            _service = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService));
            _changeService = (IComponentChangeService)provider.GetService(typeof(IComponentChangeService));

            if (_service != null && _changeService != null && value is LabelTextCollection)
            {
                LabelTextCollection property = (LabelTextCollection)value;

                LabelTextCollectionForm form = new LabelTextCollectionForm() { Items = property.ToArray() };

                if (_service.ShowDialog(form) == DialogResult.OK)
                {
                    property.FromArray(form.Items);
                    value = property;
                    _changeService.OnComponentChanged(value, null, null, null);
                }
            }
        }

        return value;
    }

    public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
    {
        return UITypeEditorEditStyle.Modal;
    }
}

Serializer

public class LabelTextCollectionSerializer : CodeDomSerializer
{
    public override object Serialize(IDesignerSerializationManager manager, object value)
    {
        var baseSerializer = (CodeDomSerializer)manager.GetSerializer( typeof(LabelTextCollection).BaseType, typeof(CodeDomSerializer));
        object codeObject = baseSerializer.Serialize(manager, value);

        if (codeObject is CodeStatementCollection && value is LabelTextCollection)
        {
            var col = value as LabelTextCollection;
            var statements = (CodeStatementCollection)codeObject;
            statements.Add(new CodeCommentStatement("LabelTextCollection : " + col.ToString()));
        }

        return codeObject;
    }
}

Property of custom Type

[Category("Appearance")]
[Editor(typeof(LabelTextCollectionEditor), typeof(UITypeEditor))]
public LabelTextCollection Titles { get; }

EDIT :

I added a set to my Titles property and set up my project for design-time debugging, I then realized that an exception was thrown on the line

object codeObject = baseSerializer.Serialize(manager, value);

stating that the Label type isn't marked as [Serializable].

I'm assuming that the base serializer is trying to write a call to my LabelTextCollection constructor and to serialize the labels field as a parameter of it.

I tried replacing the line with

object codeObject = new CodeObject();

which got rid of the exception but didn't write anything in the designer.cs file.

I'm (once again) assuming that nothing is happening because there is no relation between the CodeObject I just created and the file (unless that relation is established after it's returned by the Serialize method ?).

As you can probably tell, I'm pretty new regarding the CodeDom stuff so how should I create this object properly ?

EDIT 2 :

I'm so dumb... I forgot the codeObject is CodeStatementCollection test...

So the comment line is writing fine, now all I need to do is to write the correct line with CodeDom and it should work fine.

If someone wants to help, I currently have added to the designer.cs file :

this.FromArray( new string[] { "TEST" } );

So I'm missing the control's and the property's names to get to my final goal.

I'll answer my own post to recapitulate what I did to fix it when that's done.

1

There are 1 answers

0
Simon Baillin On BEST ANSWER

I managed to make the serialization work as I intended so I'm going to recap what I changed from the code I originally posted.

First my property of custom type needed a set to be able to be modified by the editor.

[Editor(typeof(LabelTextCollectionEditor), typeof(UITypeEditor))]
public LabelTextCollection Titles { get; set; }

I wrongly assumed that the property's value was changing because the label's texts were effectively changing in the designer after using the editor. That was happening because the editor could access the reference to the inner label array through the use of the LabelTextCollection.FromArray method. With the setter, the property is now properly edited at design-time.

The rest of the changes are all in the serializer so i'm posting the whole updated code :

public class LabelTextCollectionSerializer : CodeDomSerializer
{
    public override object Serialize(IDesignerSerializationManager manager, object value)
    {
        CodeStatementCollection codeObject = new CodeStatementCollection();

        if (value is LabelTextCollection)
        {
            LabelTextCollection col = value as LabelTextCollection;

            // Building the new string[] {} statement with the labels' texts as parameters
            CodeExpression[] strings = new CodeExpression[col.Count()];
            for (int i = 0; i < col.Count(); i++) strings[i] = new CodePrimitiveExpression(col[i]);
            CodeArrayCreateExpression arrayCreation = new CodeArrayCreateExpression(typeof(string[]), strings);

            // Building the call to the FromArray method of the currently serializing LabelTextCollection instance
            ExpressionContext context = manager.Context.Current as ExpressionContext;
            CodeMethodInvokeExpression methodInvoke = new CodeMethodInvokeExpression(context.Expression, "FromArray", arrayCreation);

            codeObject.Add(methodInvoke);
        }

        return codeObject;
    }
}

To recap the changes I made in that class :

  • Removed the call to the baseSerializer.Serialize method to manage the whole serialization myself
  • Initializing the codeObject variable as a new CodeStatementCollection
  • Building my call to the LabelTextCollection.FromArray method using CodeDom

All of that now successfully writes the line I wanted in the Designer.cs file.

PS : Thanks to @TnTinMn for the help and the push in the right direction.

EDIT :

After thorough testing of the serializer, I realized that the labels' texts went back to their default value when rebuilding the assembly containing the LabeltextCollection type while having a design view of a form containing my custom control opened.

The reason for that was that the property of LabeltextCollection type could not be serialized because the condition value is LabelTextCollection was false in that case as there was a discrepancy between two LabelTextCollection types from different assembly versions.

To fix that, I removed any direct reference to the type and accessed the method I needed to call through the Type class.

That got me the following serializer code :

public class LabelTextCollectionSerializer : CodeDomSerializer
{
    public override object Serialize(IDesignerSerializationManager manager, object value)
    {
        CodeStatementCollection codeObject = new CodeStatementCollection();

        // Building the new string[] {} statement with the labels' texts as parameters            
        string[] texts = value.GetType().GetMethod("ToArray").Invoke(value, null) as string[];
        CodeExpression[] strings = new CodeExpression[texts.Length];
        for (int i = 0; i < texts.Length; i++) strings[i] = new CodePrimitiveExpression(texts[i]);
        CodeArrayCreateExpression arrayCreation = new CodeArrayCreateExpression(typeof(string[]), strings);

        // Building the call to the FromArray method of the currently serializing LabelTextCollection instance
        ExpressionContext context = manager.Context.Current as ExpressionContext;
        CodeMethodInvokeExpression methodInvoke = new CodeMethodInvokeExpression(context.Expression, "FromArray", arrayCreation);

        codeObject.Add(methodInvoke);

        return codeObject;
    }
}

You could still test the type of value using Type.Name but as my serializer only manages a single type, that wasn't needed in my case.