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 & MDI 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 custom serializer to do the actual conversion to and from XML and has numerous extra features, such as the ability to save/load to various targets like Stream, string, etc. It can also 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 class XmlSerializerBase<TObj, TXmlObj>. The first type parameter (TObj) indicates the Type of target object represented by the second type parameter's object Type. The second type parameter (TXmlObj) 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 (TObj) since it is the "real" object affected by the layout, and the type XmlDockSiteLayout is the second type parameter (TXmlObj) since it is the root object that is serialized.
The RootNode property stores the root object that will be serialized, and the serializer is responsible for translating properties between the "real" object and the RootNode.
During serialization, the properties of the "real" object must be transferred to the object that will be serialized and assigned to the RootNode property. Override the CreateRootNodeFor method to create the root XML node that will be serialized (like an XmlDockSiteLayout) for the passed object.
During deserialization, the properties of the deserialized object (defined by the RootNode property) must be transferred to the "real" object. Override the ApplyTo method with code that examines the RootNode property value and updates the passed object (like a DockSite instance).
The following expands on the example of the DockSiteLayoutSerializer to include some of the additional configuration:
public class DockSiteLayoutSerializer
: XmlSerializerBase<DockSite, XmlDockSiteLayout> {
/// <summary>
/// Initializes an instance of the class.
/// </summary>
public DockSiteLayoutSerializer(XmlDockSiteLayout? layout = null) : base(layout) {
// Optionally register serializable objects used by this serializer
}
/// <summary>
/// Applies the information contained within this serializer to the specified object.
/// </summary>
/// <param name="obj">The object to update with deserialized information.</param>
public override void ApplyTo(DockSite obj) {
if (RootNode == null)
return;
// Copy the XmlDockSiteLayout properties from the RootNode to the DockSite instance
}
/// <summary>
/// Creates a root node for the specified object.
/// </summary>
/// <param name="obj">The object for which to create a root node.</param>
public override XmlDockSiteLayout CreateRootNodeFor(DockSite obj) {
var layout = new XmlDockSiteLayout();
// Copy the DockSite properties to a new XmlDockSiteLayout object for serialization
return layout;
}
...
}
Creating the Serializable Objects
Next, create 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 (e.g., PointToString and StringToPoint). It also defines a Tag property, useful for persisting custom data via the serialization and deserialization events that are raised (see below).
Tip
It is recommended that serializable objects should start with Xml as a naming convention and be located within a Serialization child namespace to help differentiate them from the "real" objects they represent.
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.
Unless XmlSerializer has been enabled (which is not enabled by default), all serialized properties for a custom object must be explicitly declared. The XmlObjectBase class defines a virtual GetSerializerProperties method that you can override to declare any property to be serialized by that object. This method returns an enumerable of IXmlSerializerProperty instances that describe each property.
For convenience, this IXmlSerializerProperty interface has been implemented on the XmlSerializerProperty<TSource,TProperty> class. The first type argument (TSource) indicates the Type of the object which defines the property. The second type argument (TProperty) defines the Type of value represented by the property. The constructor for this class can be passed an expression which identifies the property. Optionally, a delegate can also be passed which can determine, at run-time, if the property should be serialized.
The following example demonstrates some of the properties supported by the XmlDockSiteLayout class:
public override IEnumerable<IXmlSerializerProperty> GetSerializerProperties() {
// Make sure properties from the base class are included
foreach (var property in base.GetSerializerProperties())
yield return property;
yield return new XmlSerializerProperty<XmlDockSiteLayout, XmlAutoHideContainers?>(x => x.AutoHideContainers);
yield return new XmlSerializerProperty<XmlDockSiteLayout, XmlObjectBase?>(x => x.Content);
yield return new XmlSerializerProperty<XmlDockSiteLayout, List<XmlDockHost>>(x => x.DockHosts, x => x.ShouldSerializeDockHosts());
yield return new XmlSerializerProperty<XmlDockSiteLayout, List<XmlDocumentWindow>>(x => x.DocumentWindows, x => x.ShouldSerializeDocumentWindows());
yield return new XmlSerializerProperty<XmlDockSiteLayout, DockSiteSerializationBehavior>(x => x.SerializationFormat);
yield return new XmlSerializerProperty<XmlDockSiteLayout, List<XmlToolWindow>>(x => x.ToolWindows, x => x.ShouldSerializeToolWindows());
yield return new XmlSerializerProperty<XmlDockSiteLayout, int>(x => x.Version, x => x.ShouldSerializeVersion());
}
Important
When XmlSerializer is enabled by calling the EnableXmlSerializer method, reflection will be used to determine properties instead of the GetSerializerProperties method. The XmlSerializer will serialize all public properties by default.
Finally, the root serializer class must be configured to recognize these custom objects. Within the constructor of the root serializer class, call the base RegisterType method for each Type that needs to be supported. The Type defined by the second type parameter (TXmlObj or XmlDockSiteLayout in these examples) is automatically registered.
The following shows a partial example of how the DockSiteLayoutSerializer class is configured to support serializable objects like XmlDocumentWindow and XmlToolWindow:
public class DockSiteLayoutSerializer
: XmlSerializerBase<DockSite, XmlDockSiteLayout> {
/// <summary>
/// Initializes an instance of the class.
/// </summary>
public DockSiteLayoutSerializer(XmlDockSiteLayout? layout = null) : base(layout) {
// Register serializable objects used by this serializer
RegisterType<XmlDocumentWindow>();
RegisterType<XmlToolWindow>();
...
}
...
}
Serializing and Deserializing
The XmlSerializerBase<TObj, TXmlObj> class provides helper methods for easily serializing objects to and deserializing objects from XML as well as 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 an 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<TObj, TXmlObj>.ObjectSerialized event. When data is serialized, 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 a similar process. Create an event handler with the same argument type and attach it to the XmlSerializerBase<TObj, TXmlObj>.ObjectDeserialized event. When an object is deserialized, your method will be called passing the same Node and Item arguments. Use the Tag property of the Node read your custom data back in.
Tip
For a good example of serializing custom data, see the "Docking & MDI Layout Serialization" QuickStart.
Custom Data Types
Sometimes you may be using custom data types in the data that is serialized and deserialized. The serializer needs to know about custom data types so that it can properly map XML tags to .NET types.
The XmlSerializerBase<TObj, TXmlObj> has a RegisterType method that allows you to specify custom data types. Unless XmlSerializer has been enabled (which is not enabled by default), the serializable properties for a custom type must also be provided when registering. These serializable properties are defined using the same IXmlSerializerProperty used by serializable objects that inherit from XmlObjectBase.
Note
If the custom data type derives from XmlObjectBase, it should override the XmlObjectBase.GetSerializerProperties method instead of providing those properties during registration. The type must still be registered, but the additional property data does not need to be specified (e.g., serializer.RegisterType<CustomData>();)
This sample code shows how to register a sample CustomData type with the serializer:
serializer.RegisterType<CustomData>(GetCustomDataSerializerProperties());
IEnumerable<IXmlSerializerProperty> GetCustomDataSerializerProperties() {
yield return new XmlSerializerProperty<CustomData, string>(x => x.MyStringProperty);
yield return new XmlSerializerProperty<CustomData, int>(x => x.MyIntProperty, x => x.ShouldSerializeMyIntProperty());
...
}
Enable XmlSerializer
By default, the XmlSerializer is not used for serialization since it does not support AOT trimming. We recommend all users move away from relying on XmlSerializer logic for custom data serialization and adopt the new declarative approach. If necessary, you can enable an XmlSerializer-based solution by calling the EnableXmlSerializer method. When enabled, reflection will be used to discover serializable properties and additional configuration of properties on registered type (using IXmlSerializerProperty) is not necessary.
Warning
The EnableXmlSerializer method is intended to support backwards compatibility and provide an opportunity for developers to choose the best time to migrate to the new approach. We plan to deprecate this method and all XmlSerializer support in a future release. Please contact support to resolve any conditions with the new serializer that may be blocking a transition away from XmlSerializer.
Caution
An issue has been discovered where Microsoft's .NET implementation of 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.