Serialization
The ActiproSoftware.Windows.Serialization namespace contains several classes that are helpful for persisting hierarchies of data (such as for control layouts) in XML.
Saving/Loading Object Hierarchies from XML
There are countless cases where it is useful to persist a hierarchy of data to XML that can be saved and reloaded later.
One example of this is storing the layout of a customizable control such as a Docking DockSite, where the end user can customize the layout of tool windows. The layout needs to be saved and restored between application sessions so that their customizations are kept intact.
The Shared Library has a complete framework for supporting easy serialization and deserialization of a hierarchy of objects (such as layout data) to XML. The framework uses a standard XmlSerializer
to do the actual conversion to and from XML, but the framework has numerous extra features, such as that ability to save/load to various targets like Stream
s, strings, etc. It also can raise an event any time an object is serialized or deserialized, allowing you to easily store and retrieve custom data anywhere in the serialized output.
Creating the Root Serializer
The first step in creating a serializable hierarchy is making the root serializer class.
This class should inherit the base generic XmlSerializerBase<T, U> class. The first type parameter indicates the Type
of target object represented by the second type parameter's object Type
. The second type parameter indicates the Type
of the root object that will be serialized and must inherit XmlObjectBase.
For instance DockSite's layout serialization class, DockSiteLayoutSerializer, is defined as:
public class DockSiteLayoutSerializer
: XmlSerializerBase<DockSite, XmlDockSiteLayout> { ... }
The type DockSite is the first type parameter since it is the "real" object affected by the layout, and the type XmlDockSiteLayout is the second type parameter since it is the root object that is serialized.
Next there are three methods to override. First, override GetXmlSerializer to return a standard XmlSerializer
that specifies the Type
s that will be serialized/deserialized.
Second, override ApplyTo with code that examines the RootNode property value and updates the passed object (like a DockSite instance).
Third, override CreateRootNodeFor to create the root XML node that will be serialized (like a XmlDockSiteLayout ) for the passed object.
Creating the Serializable Objects
Next, create the objects that will be part of the hierarchy to serialize. The objects must inherit XmlObjectBase. This base class provides several helper methods like converting Point
, Size
, and Rect
objects to and from strings. It also defines a Tag property, useful for persisting custom data via the serialization and deserialization events that are raised (see below).
The serializable objects should start with Xml
as a naming convention and be located within a Serialization
child namespace.
Note
Use the standard XML serialization attributes on the types and members you define, such as XmlType
, XmlElement
, XmlAttribute
, etc. These attributes help control the XML output during serialization. Remember that all public properties will be serialized.
Serializing and Deserializing
The XmlSerializerBase<T, U> class provides helper methods for easily serializing objects to and deserializing objects from XAML.
The XmlSerializerBase<T, U> class has these important members:
Member | Description |
---|---|
LoadFromFile Method | Deserializes an object from the specified file. |
LoadFromStream Method | Deserializes an object from the specified Stream . |
LoadFromString Method | Deserializes an object from the specified XML string. |
LoadFromXmlReader Method | Deserializes an object from the specified XmlReader . |
SaveToFile Method | Serializes the specified object to XML within a file. |
SaveToStream Method | Serializes the specified object to XML within a Stream . |
SaveToString Method | Serializes the specified object to a XML string. |
SaveToXmlWriter Method | Serializes the specified object to XML by using an XmlWriter . |
This sample code shows how to save a DockSite layout to an XML string:
static DockSiteLayoutSerializer? serializer;
...
serializer ??= new DockSiteLayoutSerializer();
string layout = serializer.SaveToString(dockSite);
This sample code shows how to load a DockSite layout from the XML string:
static DockSiteLayoutSerializer? serializer;
...
serializer ??= new DockSiteLayoutSerializer();
serializer.LoadFromString(layout, dockSite);
Serializing/Deserializing Custom Data
A key benefit of using the Shared Library's XML serialization framework is that custom data can be inserted anywhere within the serialized data via the handling of an event in the application that calls for the serialization.
This is particularly useful when the developer calling the serialization code didn't write it and doesn't have access to change its code.
To enable insertion of custom data, create an event handler that accepts an ItemSerializationEventArgs argument. Then attach the event handler to the XmlSerializerBase<T, U>.ObjectSerialized event. When you go to serialize data, your method will be called after each object in the hierarchy is serialized.
The Node
property in the event arguments provides the XmlObjectBase that is being serialized, and that represents the serializable data for the object indicated in the Item
property. You can set the Tag
property of the Node
to save any custom data with the serialized data.
Deserialization is the same process. Create an event handler with the same argument type and attach it to the XmlSerializerBase<T, U>.ObjectDeserialized event. Your method will be called, passing the same sort of arguments, whenever an object is deserialized. So here you can read your custom data back in.
Custom Data Types
Sometimes you may be using custom data types in the data that is serialized and deserialized. The XML serializer needs to know about custom data types so that it can properly map XML tags to .NET types. The XmlSerializerBase<T, U> has a CustomTypes property that allows you to specify custom data types, thereby preventing any exceptions such as:
The type <YourTypeHere> was not expected. Use the XmlInclude or SoapInclude attribute to specify types that are not known statically.
This sample code shows how to register a CustomData
type with the serializer, thereby preventing the above exception when performing serialization:
serializer.CustomTypes.Add(typeof(CustomData));
Optimal Memory Utilization when Using Layout Serializers
The XmlSerializerBase<T, U> class uses an XmlSerializer
as the core .NET object that reads/writes XML data. One issue that has been discovered in Microsoft's .NET implementation is that XmlSerializer
is capable of creating memory leaks, primarily whenever new instances of XmlSerializer
are created.
To combat this leak, we've implemented some caching code on our end, but also highly recommend that instead of creating a new layout serializer any time you do a layout serialization, you instead keep a reference to a single app-wide instance of the layout serializer and use that for each layout serialization.