【问题标题】:Design time serialization of custom type property自定义类型属性的设计时间序列化
【发布时间】:2020-10-20 13:55:14
【问题描述】:

所以基本上我有一个自定义 UserControl 包含一个私有数组 Label 对象,我希望能够从外部专门访问它们的 Text 属性。

因此,我添加了一个属性,其类型为 LabelTextCollectionIEnumerable 的实现,并将我的 Label 数组作为其内部列表。此外,我添加了UITypeEditor 的实现,以允许从 Windows 窗体设计器进行编辑。

为了试一试,我在表单中添加了我的控件并编辑了属性的值。在我关闭并重新打开设计器并且标签取回它们的默认值之前,所有这些都可以正常工作。

环顾四周后,我似乎必须添加CodeDomSerializer 的实现,以允许我的类型在设计时成功序列化到{Form}.Designer.cs 文件中。我尝试先序列化一个注释行来测试它,但没有生成代码。

我的最终目标是有一条类似的线

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

在使用我的编辑器修改属性后在设计时添加。 我有什么误解和/或做错了什么?

自定义类型

[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;
        }
    }
}

类型编辑器

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;
    }
}

序列化器

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;
    }
}

自定义类型的属性

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

编辑:

我在我的Titles 属性中添加了一个set 并设置了我的项目以进行设计时调试,然后我意识到在线上抛出了一个异常

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

说明Label 类型未标记为[Serializable]

我假设基本序列化程序正在尝试编写对我的LabelTextCollection 构造函数的调用并将labels 字段序列化为它的参数。

我尝试用

替换该行
object codeObject = new CodeObject();

它摆脱了异常,但没有在designer.cs 文件中写入任何内容。

我(再一次)假设什么都没有发生,因为我刚刚创建的 CodeObject 与文件之间没有任何关系(除非该关系在 Serialize 方法返回后建立?)。

您可能会说,我对 CodeDom 的东西还很陌生,所以我应该如何正确地创建这个对象?

编辑 2:

我太笨了...我忘记了codeObject is CodeStatementCollection 测试...

所以注释行写得很好,现在我需要做的就是用 CodeDom 写正确的行,它应该可以正常工作。

如果有人想帮忙,我目前已添加到designer.cs 文件中:

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

所以我错过了控件和属性的名称来达到我的最终目标。

我会回答我自己的帖子,以概括我在完成后为修复它所做的工作。

【问题讨论】:

  • public LabelTextCollection Titles { get; } 是错字吗?您声明 LabelTextCollectionEditor 有效,但您不应在 propertygrid 中编辑只读属性。 LabelTextCollectionSerializer 看起来很好,假设它被调用,它应该添加注释行。
  • 如果您还没有这样做,我建议您将项目设置为调试设计器代码。有关说明,请参阅:Set up the project for design-time debugging
  • 您好,感谢您的帮助。我确实将该属性设置为只读,因为从外部设置它可能会将它与我初始化它的标签断开,我的错。我假设因为标签的文本改变了属性的值也改变了,但我没有想到编辑器仍然可以在不设置属性的情况下访问对标签的引用。
  • 查看帖子编辑以了解进度更新。
  • 我上次做这类工作已经十年了,但是“Serializable”错误似乎很奇怪。请澄清以下内容:1)是Label类型System.Windows.Forms.Label或您自己的类型; 2) 这是一个 .Net Core 项目吗? 3)请在问题中添加完整的异常消息和堆栈跟踪; 4)baseSerializer返回什么类型,即baseSerializer .GetType().FullName。我错过了LabelTextCollection 没有无参数构造函数;如果没有,您在调用 baseSerializer.Serialize 时通常会收到 null。

标签: c# winforms serialization windows-forms-designer uitypeeditor


【解决方案1】:

我设法使序列化按预期工作,因此我将回顾一下我对最初发布的代码所做的更改。

首先我的自定义类型的属性需要一个能够被编辑器修改的集合。

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

我错误地认为属性的值正在改变,因为标签的文本在使用编辑器后在设计器中有效地改变了。 这是因为编辑器可以通过使用LabelTextCollection.FromArray 方法访问对内部标签数组的引用。 使用 setter,现在可以在设计时正确编辑属性。

其余的更改都在序列化程序中,所以我发布了整个更新的代码:

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;
    }
}

回顾一下我在该课程中所做的更改:

  • 删除了对 baseSerializer.Serialize 方法的调用以自行管理整个序列化
  • codeObject 变量初始化为new CodeStatementCollection
  • 使用 CodeDom 构建我对 LabelTextCollection.FromArray 方法的调用

所有这些现在都成功地在Designer.cs 文件中写入了我想要的行。

PS: 感谢@TnTinMn 的帮助和朝着正确方向的推动。

编辑:

在对序列化程序进行彻底测试后,我意识到标签的文本在重建包含 LabeltextCollection 类型的程序集时返回到它们的默认值,同时打开了包含我的自定义控件的表单的设计视图。

原因是LabeltextCollection 类型的属性无法序列化,因为在这种情况下条件value is LabelTextCollection 为假,因为来自不同程序集版本的两个LabelTextCollection 类型之间存在差异。

为了解决这个问题,我删除了对该类型的任何直接引用,并通过Type 类访问了我需要调用的方法。

这让我得到了以下序列化程序代码:

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;
    }
}

您仍然可以使用Type.Name 测试value 的类型,但由于我的序列化程序只管理一个类型,因此在我的情况下不需要。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2010-11-18
    • 2018-03-03
    • 2015-07-27
    • 1970-01-01
    • 1970-01-01
    • 2017-03-05
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多