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.
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.
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 :
To recap the changes I made in that class :
baseSerializer.Serialize
method to manage the whole serialization myselfcodeObject
variable as anew CodeStatementCollection
LabelTextCollection.FromArray
method using CodeDomAll 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 conditionvalue is LabelTextCollection
was false in that case as there was a discrepancy between twoLabelTextCollection
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 :
You could still test the type of
value
usingType.Name
but as my serializer only manages a single type, that wasn't needed in my case.