Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to tell XmlSerializer to serialize properties with [DefautValue(...)] always?

I am using DefaultValue attribute for the proper PropertyGrid behavior (it shows values different from default in bold). Now if I want to serialize shown object with the use of XmlSerializer there will be no entries in xml-file for properties with default values.

What is the easiest way to tell XmlSerializer to serialize these still?

I need that to support "versions", so when I change default value later in the code - serialized property gets value it had serialized with, not "latest" one. I can think about following:

  • Override behavior of PropertyGrid (use custom attribute, so it will be ignoreed by XmlSerializer);
  • Do sort of a custom xml-serialization, where ignore DefaultValue's;
  • Do something with object before passing it to XmlSeriazer so it won't contain DefaultValue's anymore.

But there is a chance I miss some secret property what allows to do it without much pain =D.

Here is an example of what I want:

    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

If I uncomment (*1), then I have desired effect in PropertyGrid - properties with default values are displayed in normal text, otherwise text is bold. However XmlSerializer will NOT put properties with default value into xml-file and this is BAD (and I am trying to fix it).

If I uncomment (*2) and (*3), then it's totally same as uncommenting (*1).

If I uncomment (*2) and (*4), then XmlSerializer will always put properties into xml-file, but this happens because they do not have default value anymore and PropertyGrid shows all values in bold text.

like image 250
Sinatr Avatar asked Mar 12 '13 09:03

Sinatr


4 Answers

As long as you don't need attributes in your Xml, if you use the DataContractSerializer instead you will get the behavior you desire.

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

produces (minus namespaces etc)

<Test>
    <AllowNegative>false</AllowNegative>
</Test>
like image 194
Phil Avatar answered Oct 23 '22 03:10

Phil


You could use two properties:

// 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();
    }
}
like image 41
Daniel Avatar answered Oct 23 '22 05:10

Daniel


I believe what you are looking for is ShouldSerialize() and Reset(). Using these will expand your class a bit more (with two functions per property), however, it achieves specifically what you are looking for.

Here's a quick example:

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

Note that because you want to keep the PropertyGrid functionality in tact, you must know whether you are serializing or not when the ShouldSerialize() function is called. I suggest you implement some sort of control flag that gets set when serializing, and thus always return true.


Please note that you cannot use the DefaultValue attribute in conjunction with the ShouldSerialize() and Reset() functions (you only use either or).


Edit: Adding clarification for the ShouldSerialize() function.

Because there is currently no way to serialize a default value and let the PropertyGrid know that a property has its default value, you must implement a condition checking whether you are in design mode.

Assuming your class derives from a Component or Control, you have a DesignMode property which is set by Visual Studio at design time only. The condition looks as follows:

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

Edit 2: We're not talking about Visual Studio's design mode.

With the above code in mind, create another property called IsSerializing. Set the IsSerializing property to true before calling XmlSerializer.Serialize, and unset it after.

Finally, change the if (!DesignMode) conditional statement to be if (IsSerializing).

like image 3
Jesse Avatar answered Oct 23 '22 03:10

Jesse


This behaviour of the XmlSerializer can can be overwritten with XmlAttributeOverrides

I borrowed the idea from 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;
}

And made my self an an Attribute to decorate the classes which should emit the default values. If you want do this for all classes, I'm sure you can adapt the whole concept.

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

Because xsd.exe produces partial classes you can add this EmitDefaultValuesAttribute in seperate a file:

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

<EmitDefaultValuesAttribute()>
Public MySubClass
End Class

Usage is as follows:

Dim serializer As New XmlSerializer(GetType(MyClass), EmitDefaultValuesAttribute.GetOverrides(GetType(MyClass).Assembly))
like image 1
dummy Avatar answered Oct 23 '22 05:10

dummy