如何告诉 XmlSerializer 总是用 [DefautValue(...)] 序列化属性?

Posted

技术标签:

【中文标题】如何告诉 XmlSerializer 总是用 [DefautValue(...)] 序列化属性?【英文标题】:How to tell XmlSerializer to serialize properties with [DefautValue(...)] always? 【发布时间】:2021-05-15 23:14:39 【问题描述】:

我正在使用DefaultValue 属性来实现正确的PropertyGrid 行为(它以粗体显示与默认值不同的值)。现在,如果我想使用 XmlSerializer 序列化显示的对象,则 xml 文件中将没有默认值属性的条目。

告诉 XmlSerializer 序列化这些的最简单方法是什么?

我需要它来支持“版本”,所以当我稍后在代码中更改默认值时 - 序列化的属性会获取它已经序列化的值,而不是“最新的”。我可以考虑以下:

覆盖PropertyGrid的行为(使用自定义属性,所以XmlSerializer会忽略它); 做一些自定义的 xml 序列化,忽略 DefaultValue's; 在将对象传递给 XmlSeriazer 之前对它做一些事情,这样它就不会再包含 DefaultValue 了。

但是我有可能会错过一些秘密属性,它可以让我轻松地做到这一点 =D。

这是我想要的一个例子:

    private bool _allowNegative = false;
    /// <summary>
    /// Get or set if negative results are allowed
    /// </summary>
    [Category(CategoryAnalyse)]
    [Admin]
    [TypeConverter(typeof(ConverterBoolOnOff))]
    //[DefaultValue(false)] *1
    public bool AllowNegative
    
        get  return _allowNegative; 
        set
        
            _allowNegative = value;
            ConfigBase.OnConfigChanged();
        
    
    //public void ResetAllowNegative()  _allowNegative = false;  *2
    //public bool ShouldSerializeAllowNegative()  return _allowNegative;  *3
    //public bool ShouldSerializeAllowNegative()  return true;  *4

如果我取消注释 (*1),那么我将在 PropertyGrid 中获得所需的效果 - 具有默认值的属性以普通文本显示,否则文本为粗体。但是XmlSerializerNOT 将具有默认值的属性放入 xml 文件中,这是 BAD(我正在尝试修复它)。

如果我取消注释 (*2) 和 (*3),那么它与取消注释 (*1) 完全一样。

如果我取消注释 (*2) 和 (*4),则 XmlSerializer 将始终将属性放入 xml 文件中,但这是因为它们不再具有默认值和 @987654333 @ 以粗体显示所有值。

【问题讨论】:

我还怀疑即使您将某个属性明确设置为默认值,XMLSerializer 仍然不会序列化数据,因为该值与默认值匹配。这意味着我认为您的解决方案将更像是一个自定义序列化程序。 @BradRem 也在思考这应该是正确的做法。遗憾的是,相同的属性被用于不同的目的(受到不同的威胁)而没有提前考虑。我宁愿写[XmlDefaultValue(...)][PropertyGridDefaultValue(...)]。尽管编写更多属性很无聊,但最终用户可以选择他所需要的。 【参考方案1】:

只要您不需要 Xml 中的属性,如果您使用 DataContractSerializer 代替,您将获得您想要的行为。

[DataContract]
public class Test

    [DataMember]
    [DefaultValue(false)]
    public bool AllowNegative  get; set; 


void Main()

    var sb2 = new StringBuilder();
    var dcs = new DataContractSerializer(typeof(Test));

    using(var writer = XmlWriter.Create(sb2))
    
        dcs.WriteObject(writer, new Test());
    

    Console.WriteLine(sb2.ToString());  

产生(减去命名空间等)

<Test>
    <AllowNegative>false</AllowNegative>
</Test>

【讨论】:

看起来非常优雅和聪明的解决方案(你不再使用XmlSerialization,而是产生看起来像它的东西)。我唯一的问题是XmlSerialization 是否可以反序列化DataContractSerializer 输出而没有问题。还要考虑Xml... 属性(我不使用atm),比如XmlElement... @Sinatr:有什么原因不能用 DataContractSerializer 反序列化? 大声笑,也没有考虑过这个=D试图在atm测试它。 好的,经过一些尝试,它原则上工作得更好——如果你不将[DataContract]添加到类中,那么所有公共属性都会被序列化..包括那些没有设置器的=DI面临几个问题:1)属性得到排序,我可以禁用排序(我希望属性以与类中定义的顺序相同的顺序出现)2)它是单行xml,有没有简单的方法来拆分它成行(不包括后处理)?另外我的问题是读取已经使用XmlSerializer 序列化的数据,但我认为为此我可以简单地尝试反序列化为合同...... .. 如果它不起作用 - 然后使用 XmlSerializer (用于向后兼容)。序列化将始终与合同一起发生。所以唯一关键的是如何在 atm 中添加缩进。【参考方案2】:

您可以使用两个属性:

// For property grid only:
[Category(CategoryAnalyse)]
[TypeConverter(typeof(ConverterBoolOnOff))]
[DefaultValue(false)]
[XmlIgnore]
public bool AllowNegative

    get  return _allowNegative; 
    set
    
        _allowNegative = value;
        ConfigBase.OnConfigChanged();
    


// For serialization:
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
[TypeConverter(typeof(ConverterBoolOnOff))]
[XmlElement("AllowNegative")]
public bool AllowNegative_XML

    get  return _allowNegative; 
    set
    
        _allowNegative = value;
        ConfigBase.OnConfigChanged();
    

【讨论】:

谢谢。当需要序列化数组(首先将它们转换为紧凑字符串)时,我正在使用类似的想法。问题是(如果我决定对每个属性都采用这种方式)当您从外部访问类(使用InteliSense ofc)时会有很多可重复的属性而不是这样一点点工作。 @Sinatr:您可以使用EditorBrowsableAttribute 隐藏 IntelliSense 中的重复属性。 In Visual C#, EditorBrowsableAttribute does not suppress members from a class in the same assembly.【参考方案3】:

我相信您正在寻找的是ShouldSerialize() and Reset()。使用这些将进一步扩展您的类(每个属性有两个函数),但是,它可以具体实现您正在寻找的东西。

这是一个简单的例子:

// your property variable
private const String MyPropertyDefault = "MyValue";
private String _MyProperty = MyPropertyDefault;

// your property
// [DefaultValueAttribute("MyValue")] - cannot use DefaultValue AND ShouldSerialize()/Reset()
public String MyProperty

    get  return _MyProperty; 
    set  _MyProperty = value; 



// IMPORTANT!
// notice that the function name is "ShouldSerialize..." followed
// by the exact (!) same name as your property
public Boolean ShouldSerializeMyProperty()

    // here you would normally do your own comparison and return true/false
    // based on whether the property should be serialized, however,
    // in your case, you want to always return true when serializing!

    // IMPORTANT CONDITIONAL STATEMENT!
    if (!DesignMode)
        return true; // always return true outside of design mode (is used for serializing only)
    else
        return _MyProperty != MyPropertyDefault; // during design mode, we actually compare against the default value


public void ResetMyProperty()

    _MyProperty = MyPropertyDefault;

请注意,由于您希望保持 PropertyGrid 功能完整,因此您必须知道在调用 ShouldSerialize() 函数时是否正在序列化。我建议你实现某种在序列化时设置的控制标志,因此总是return true


请注意,您不能DefaultValue 属性与ShouldSerialize()Reset() 函数结合使用(您只能使用或)。 p>


编辑:ShouldSerialize() 函数添加说明。

由于目前没有方法可以序列化默认值并让 PropertyGrid 知道属性具有其默认值,因此您必须实现条件检查您是否处于设计模式。

假设您的类派生自 ComponentControl,则您有一个 DesignMode 属性,该属性由 Visual Studio 在设计时设置。条件如下:

if (!DesignMode)
    return true; // always return true outside of design mode (is used for serializing only)
else
    return _MyProperty != MyPropertyDefault; // during design mode, we actually compare against the default value

编辑 2:我们不是在谈论 Visual Studio 的设计模式。

记住上面的代码,创建另一个名为IsSerializing 的属性。将IsSerializing 属性设置为true之前 调用XmlSerializer.Serialize,然后取消设置它之后

最后,将if (!DesignMode)条件语句改为if (IsSerializing)

【讨论】:

您链接到的文档说“要么应用 DefaultValueAttribute 要么提供 ResetPropertyName 和 ShouldSerializePropertyName 方法。不要同时使用两者。”如果这是该规则的有效例外(我不知道),您可能应该提供更多信息,说明为什么该规则不适用于此处。 @hvd 谢谢!我已经更新了我的答案。我显然从那个笔记中得到了错误的理解。 谢谢,这很有趣,很高兴知道。我也很欣赏你为如此清晰地描述它所做的努力。但是仍然存在一个问题:PropertyGrid 和 XmlSerializer 都使用 DefaultValueShouldSerialize../Reset.. 来确定默认值,并且 我希望 PropertyGrid 在属性值为默认值时显示正常而不是粗体文本这对于用户快速查看已更改的数十个选项非常有用,但是 我希望 XmlSerializer 同时始终序列化所有属性,即使它们具有默认值。而且这个解决方案没有帮助 在问题文本中添加了解释 @Jesse,那你误解了我,我不是在谈论Designer time,而是在谈论PropertyGrid 组件,所以就我而言,我从来没有在DesignMode 中。实际上,现在您在控制变量的帮助下给了我一个很好的想法,我可以在序列化之前设置。理论上应该有效。给我一分钟检查一下。【参考方案4】:

XmlSerializer 的这种行为可以用 XmlAttributeOverrides

我借鉴了here的想法:

static public XmlAttributeOverrides GetDefaultValuesOverrides(Type type)

    XmlAttributeOverrides explicitOverrides = new XmlAttributeOverrides();

    PropertyDescriptorCollection c = TypeDescriptor.GetProperties(type);
    foreach (PropertyDescriptor p in c)
    
        AttributeCollection attributes = p.Attributes;
        DefaultValueAttribute defaultValue = (DefaultValueAttribute)attributes[typeof(DefaultValueAttribute)];
        XmlIgnoreAttribute noXML = (XmlIgnoreAttribute)attributes[typeof(XmlIgnoreAttribute)];
        XmlAttributeAttribute attribute = (XmlAttributeAttribute)attributes[typeof(XmlAttributeAttribute)];

        if ( defaultValue != null && noXML == null )
        
            XmlAttributeAttribute xmlAttribute = new XmlAttributeAttribute(attribute.AttributeName);
            XmlAttributes xmlAttributes = new XmlAttributes();
            xmlAttributes.XmlAttribute = xmlAttribute;
            explicitOverrides.Add(userType, attribute.AttributeName, xmlAttributes);
        
    
    return explicitOverrides;

并让我自己成为一个属性来装饰应该发出默认值的类。 如果您想对所有课程都这样做,我相信您可以调整整个概念。

Public Class EmitDefaultValuesAttribute
    Inherits Attribute
    Private Shared mCache As New Dictionary(Of Assembly, XmlAttributeOverrides)

    Public Shared Function GetOverrides(assembly As Assembly) As XmlAttributeOverrides
        If mCache.ContainsKey(assembly) Then Return mCache(assembly)
        Dim xmlOverrides As New XmlAttributeOverrides
        For Each t In assembly.GetTypes()
            If t.GetCustomAttributes(GetType(EmitDefaultValuesAttribute), True).Count > 0 Then
                AddOverride(t, xmlOverrides)
            End If
        Next
        mCache.Add(assembly, xmlOverrides)
        Return xmlOverrides
    End Function

    Private Shared Sub AddOverride(t As Type, xmlOverrides As XmlAttributeOverrides)
        For Each prop In t.GetProperties()
            Dim defaultAttr = prop.GetCustomAttributes(GetType(DefaultValueAttribute), True).FirstOrDefault()
            Dim xmlAttr As XmlAttributeAttribute = prop.GetCustomAttributes(GetType(XmlAttributeAttribute), True).FirstOrDefault()
            If defaultAttr IsNot Nothing AndAlso xmlAttr IsNot Nothing Then
                Dim attrs As New XmlAttributes '= New XmlAttributeAttribute
                attrs.XmlAttribute = xmlAttr
                ''overide.Add(t, xmlAttr.AttributeName, attrs)
                xmlOverrides.Add(t, prop.Name, attrs)
            End If
        Next
    End Sub

因为xsd.exe 产生部分类,你可以在单独的文件中添加这个EmitDefaultValuesAttribute

<EmitDefaultValuesAttribute()>
Public MyClass
    Public Property SubClass() As MySubClass
End Class

<EmitDefaultValuesAttribute()>
Public MySubClass
End Class

用法如下:

Dim serializer As New XmlSerializer(GetType(MyClass), EmitDefaultValuesAttribute.GetOverrides(GetType(MyClass).Assembly))

【讨论】:

在序列化过程中听起来像是对给定对象的动态属性操作。我不太清楚它是如何工作的,我也不懂 100% vb.net 语法,所以无法弄清楚如何从你的代码中 hide DefaultValue (但我猜只是不将其添加到某处或替换为 dummy 属性)。仍然值得点赞,谢谢!

以上是关于如何告诉 XmlSerializer 总是用 [DefautValue(...)] 序列化属性?的主要内容,如果未能解决你的问题,请参考以下文章

在C#中 如何序列化图片 用System.Xml.Serialization.XmlSerializer这个可以吗?如果可以的话怎样使用?

XmlSerializer和复杂前缀

如何使 System.Web XmlSerializer 序列化程序编码引号 c#

c# XmlSerializer 反序列化器缺少默认命名空间

C# XmlSerializer 用不同的命名空间序列化同一个类

安卓开发之利用XmlSerializer生成XML文件