Using The Data Serializer

MonoDevelop provides a general purpose data serializer which can be used by add-ins to serialize data to files. This serializer is used by the project service to read and write projects and solutions, but it can be used to store all kinds of information. This API is available in the MonoDevelop.Core.Serialization namespace. The serializer is implemented in the DataSerializer class.

DataSerializer is similar to the core XML serializer, in which the process of serialization can be controlled by applying some attributes to fields and properties. The main difference is that the data serializer does not directly generate XML, but data nodes. A DataNode can be either a DataValue (a key/value pair) or a DataItem (a collection of DataNode).

The DataNode/DataValue/DataItem model is more or less equivalent to the XmlNode/XmlAttribute/XmlElement model. It is possible to directly serialize data into XML, using the XmlDataSerializer class. This class serializes an object into XML by converting DataNode objects to XmlNode objects. So DataItems will be serialized as XmlElements and DataValue objects as XmlAttribute. The XmlConfigurationWriter and XmlConfigurationReader classes allow more fine grained serialization options.

Designing Serializable Types

The MonoDevelop.Core.Serialization namespace defines several attributes which can be used to control the way data is serialized.

Making a Class Serializable

There is no special attribute to be applied to a class to make it serializable. The only requirement for a class to be serializable is to have a default constructor (which can be private).

The DataItemAttribute attribute can be used to set the name of the type in the serialized data model. By default it’s the name of the class. This attribute is optional. Here is an example of how this attribute can be used:

Class Serialized Data
[DataItem ("DataTest")]
public class SerializationTest
{
    ...
}
<DataTest>
   ...
</DataTest>

Fields and Properties

An important difference between DataSerializer and other serializers is that fields and properties are not serialized by default. For a field or property to be serialized, the ItemPropertyAttribute attribute has to be explicitly applied to it.

The serializer generates a DataNode for each serializable member. This DataNode will be a DataValue if the member has a primitive value, or a DataItem if the member is a class (members of the class will be serialized as children of that DataItem).

ItemPropertyAttribute has several properties (all of them optional) that can be set to change the default serialization behavior:

  • Name: Name of the property in the serialized model. It’s the member name by default. You can specify a nested element name. For example, the name “Data/Name” will create the property as a child of the Data element, and it will be named “Name”.
  • DefaultValue: Default value of the member. A property won’t be serialized if the assigned value is the default value.
  • ReadOnly: true if the member will be read but never written.
  • WriteOnly: true if the member will be written but never read.
  • Scope: When added to a collection or array member, it specifies the nesting level to which the ItemPropertyAttribute applies. This is explained in more detail below.
  • ValueType: Specifies the type to use for serialization, if the one declared in the member is too generic.

For example:

Class Serialized Data
public class SerializationTest
{
    // This field won't be serialized
    int someInt;

    // This field will be serialized
    [ItemProperty]
    string someString = "Hi";

    // This field will be serialized with a custom name
    [ItemProperty ("CustomName")]
    int value = 44;

    // This field will be serialized, but only if its value
    // is not "Bye" (so it won't be serialized in this example)
    [ItemProperty (DefaultValue = "Bye")]
    string someDefaultString = "Bye";

    // This field will be serialized as a child element of "Data"
    [ItemProperty ("Data/Info")]
    string someInfo = "some info";
}
<SerializationTest
      someString = "Hi"
      CustomName = "44">
    <Data Info = "some info" />
</SerializationTest>

Serializing Lists

The DataSerializer will serialize an object as a collection of items if:

  • the object is an array
  • the type of the object implements IList, with a public Add method.

Lists are serialized as a DataItem which has a DataNode for each element of the list. The root DataItem can be customized using the ItemPropertyAttribute just like any other member of a type.

For example:

Class Serialized Data
public class SerializationTest
{
    // A simple collection of strings
    [ItemProperty ("Names")]
    List<string> names = new List<string> ();

    public SerializationTest ()
    {
        names.Add ("Jordi");
        names.Add ("Maria");
    }
}
<SerializationTest>
    <Names>
        <String>Jordi</String>
        <String>Maria</String>
    </Names>
</SerializationTest>

Customizing the Serialization of Collection Elements

By default, elements of the list are serialized using the type name of the element type as the name for the DataNode. It is also possible to customize the serialization of the list elements by using the ItemPropertyAttribute. In this case, the Scope attribute has to be used to specify that the attribute applies to the elements of the list, not to the list as a whole.

To specify that an ItemPropertyAttribute applies to the elements of a list, the Scope must be set to ”*“. Here are some examples:

Class Serialized Data
public class SerializationTest
{
    // A collection of strings with a custom name for the elements
    [ItemProperty ("ExtraNames")]
    [ItemProperty ("Name", Scope="*")]
    List<string> extraNames = new List<string> ();

    // This property will be serialized as a collection of strings.
    // The element name will be "Objects" (instead of "array"),
    // and there will be an "Id" element for each item of the list.
    // Also, the ValueType property is set to tell the serializer
    // that the elements of the collection are strings.
    [ItemProperty ("Objects")]
    [ItemProperty ("Id", Scope="*", ValueType=typeof(string))]
    ArrayList array = new ArrayList ();

    public SerializationTest ()
    {
        extraNames.Add ("Jordi");
        extraNames.Add ("Maria");
        array.Add ("Jordi3");
        array.Add ("Maria3");
    }
}
<SerializationTest>
    <ExtraNames>
        <Name>Jordi</Name>
        <Name>Maria</Name>
    </ExtraNames>
    <Objects>
        <Id>Jordi3</Id>
        <Id>Maria3</Id>
    </Objects>
</SerializationTest>

Nested Scopes

When serializing nested collections, several ItemPropertyAttribute attributes can be applied using different scopes, one for each nesting level. The scope for the first level of nesting is “*”, the scope for the second level is “*/*”, for the third “*/*/*” and so on:

Class Serialized Data
public class SerializationTest
{
    // When using nested collections, the scope can be aggregated
    [ItemProperty ("User", Scope="*")]
    [ItemProperty ("Name", Scope="*/*")]
    List<List<string>> nestedNames = new List<List<string>> ();

    public SerializationTest ()
    {
        List<string> nested1 = new List<string> ();
        nested1.Add ("Jordi1");
        nested1.Add ("Maria1");
        nestedNames.Add (nested1);
        List<string> nested2 = new List<string> ();
        nested2.Add ("Jordi2");
        nested2.Add ("Maria2");
        nestedNames.Add (nested2);
    }
}
<SerializationTest>
    <nestedNames>
        <User>
            <Name>Jordi1</Name>
            <Name>Maria1</Name>
        </User>
        <User>
            <Name>Jordi2</Name>
            <Name>Maria2</Name>
        </User>
    </nestedNames>
</SerializationTest>

Collections Without a Root Item

When the ExpandedCollectionAttribute attribute is applied to a collection, the root item of the collection will not be serialized. The elements of the collection will be serialized as direct children of the class item.

The ItemPropertyAttribute can be used to customize the serialization of the elements, although the use of the Scope property is not required here.

Class Serialized Data
public class SerializationTest
{
    [ItemProperty]
    int SomeInteger = 32;

    // This property will be serialized as a set of Value elements
    // There won't be a "values" root element.
    [ItemProperty ("Value")]
    [ExpandedCollection]
    string[] values;

    public SerializationTest ()
    {
        values = new string[] { "One", "Two" };
    }
}
<SerializationTest SomeInteger="32">
    <Value>One</Value>
    <Value>Two</Value>
</SerializationTest>

Serializing Dictionaries

The serializer can serialize objects implementing the IDicionary interface. Dictionaries are serialized as a DataItem node which has a list of dictionary entries as children. Each dictionary entry is also a DataItem (named Item by default) which has two child items: the Key item, which holds the key of the entry, and the Value item, which holds the value of the entry.

The scopes ‘item’, ‘key’ and ‘value’ can be used to customize the serialization of the corresponding elements.

Some examples:

Class Serialized Data
public class SerializationTest
{
    // A simple dictionary, with no customizations
    [ItemProperty]
    Dictionary<string,int> data = new Dictionary<string,int> ();
    // A dictionary containing objects, using custom names
    // for the items, keys and values
    [ItemProperty ("Users")]
    [ItemProperty ("User", Scope="item")]
    [ItemProperty ("Id", Scope="key")]
    [ItemProperty ("Data", Scope="value")]
    Dictionary<string,User> userData = new Dictionary<string,User> ();

    public SerializationTest ()
    {
        data ["One"] = 1;
        data ["Two"] = 2;
        userData ["jd"] = new User ("Jordi", "jordi@here.com");
        userData ["ma"] = new User ("Maria", "maria@here.com");
    }
}

public class User
{
    [ItemProperty ("Name")]
    string name;
    [ItemProperty ("EMail")]
    string email;

    public User (string name, string email) { ... }
}
<SerializationTest>
    <data>
        <Item Key="One" value="1" />
        <Item Key="Two" value="2" />
    </data>
    <Users>
        <User Id="jd">
            <Data Name="Jordi" EMail="jordi@here.com" />
        </User>
        <User Id="maria">
            <Data Name="Maria" EMail="maria@here.com" />
        </User>
    </data>
</SerializationTest>

Custom serialization

Types with complex serialization needs which can’t be specified using attributes can implement ICustomDataItem to provide a custom serialization behavior. ICustomDataItem defines two methods:

  • Serialize: This method has to serialize the contents of the object into a DataCollection. It provides as input parameter an ITypeSerializer instance which you can use to run the default serializer.
  • Deserialize: It has to deserialize the provided DataCollection and fill the object with data. An ITypeSerializer is also provided.

Here is an example:

public class WindowData: ICustomDataItem
{
    [ItemProperty]
    string id;

    [ItemProperty]
    string title;

    int width;
    int height;

    DataCollection ICustomDataItem.Serialize (ITypeSerializer serializer)
    {
        // Use the provided serializer to run the default serialization.
        // This is optional, but it's useful if you just want to
        // tweak the default behavior, not completely reimplement it.
        DataCollection data = serializer.Serialize (this);

        // Now add some custom data
        data.Add (new DataValue ("size", width + " " + height));
        return data;
    }

    void ICustomDataItem.Deserialize (ITypeSerializer serializer, DataCollection data)
    {
        // Extract the custom value we added in the Serialize method
        DataValue size = data.Extract ("size") as DataValue;
        string[] sizes = size.Value.Split (' ');
        width = int.Parse (sizes[0]);
        height = int.Parse (sizes[1]);

        // Deserialize the other fields.
        serializer.Deserialize (this, data);
    }
}