I’ve rambled about XML Serialization for a while, in particular, the fact that the built-in Serialization provided with .NET is problematic to use as a file format. This is primarily in relation to BASeBlock, which currently saves Level data using th BinaryFormatter- this introduces a problem as simply recompiling the program or making simple changes can break the reading of old file formats.
To combat this, I always wanted to switch to a more user-friendly or at least ‘general’ format. In this case, XML. But doing so was not entirely straightforward. For one thing, the existing IFormatter implementations required the classes to be built in a specific way- have default, public constructors, etc. These design choices would not make sense in my case- I don’t want things like Blocks to be instantiated with no arguments, since that would leave the standard properties in a state that can cause errors for other logic and if I can prevent it earlier I will do so (and I do).
I toyed with a IFormatter implementation I found online which alleged to support XML serialization via IFormatter to XML files. and it- sort of worked, but apparently has issues with Generic type definitions. It also used the older XmlSerializer whereas I typically prefer the very fluent Xml.Linq (XElement, XDocument).
The result was essentially just having to define my own small library and serialization framework with one straightforward task- it can either take a object and turn it into an XElement, or take that XElement and transform it into the object. We have two types of Objects that would need to be turned into and back from “XElement’s”. We have objects defined in classes that we are in control of and can change- thus we could implement new interfaces if needed- and we have objects either within the .NET Framework or in other libraries that we cannot change. The former seems fairly straightforward. We can solve that issue by defining a fairly straightforward interface not unlike the ISerializable interface:
--Edit--
I realized after originally posting that IXMLSerializable was a rather poor name choice, as that conflicts with the IXMLSerializable interface in the Framework. I have thus changed the class name to “IXmlPersistable” instead.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/// <summary> /// Indicates a object supports our XElement Serialization, serializing to an XElement. /// This also means the class must implement a constructor which accepts a single XElement parameter /// and will reconstruct the object. Classes implementing this interface should also implement a constructor that accepts a XElement parameter. /// </summary> public interface IXmlPersistable { /// <summary> /// Retrieves the XElement representation of this class instance. /// </summary> /// <returns>XElement representing this class which can be used to reconstitute it by passing it via the constructor.</returns> XElement GetXmlData(String pNodeName); } |
In this case, the interface defines a single method- GetXmlData, defined to return an XElement given the name to assign to that XElement Node. Like ISerializable it also has a added requirement that the class definition have a constructor that accepts a XElement which will be used to “reconstitute” the instance with the data in that XElement. This “solves” the issue of serializing our own class instances- but what about framework classes or classes in other libraries that we cannot make such changes to? In order to solve that issue I created another Interface, the IXmlPersistableProvider<T> interface:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/// <summary> /// Class implementation that is used to provide serialization and deserialization to and from an XElement /// for a given class. /// </summary> public interface IXmlPersistableProvider<T> { /// <summary> /// Serializes the given object to a XElement instance. /// </summary> /// <param name="sourceItem"/>Source Item to serialize to an XML Element. /// <param name="pNodeName"/>Name to give the resulting XML Node. /// <returns>XElement representing the given instance.</returns> XElement SerializeObject(T sourceItem,String pNodeName); /// <summary> /// constructs a T out of the given XElement data. /// </summary> /// <param name="xmlData"/> /// <returns>reconstuted type T from the given XElement.</returns> T DeSerializeObject(XElement xmlData); } |
This interface is implemented to create a class that can be defined to supprot serialization and deserialization of a given type when that type doesn’t support it. I then created a standard helper class which implemented a number of forms of this interface to serialize to and from the various framework classes, as well as methods which effectively encapsulate the logic of saving/loading to or from a given instance based on whether that type supports the interface or there is a provider. I have the Provider Dictionary initialized to the standard Providers, and there is a static method that can add new providers from external code, if desired (which I’ve called “Helpers”). By way of example, here is the code that saves a given instance to an XElement:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
public static XElement SaveElement<T>(T SourceData,String pNodeName) { bool implementsInterface = false; Func<T, XElement> buildfunc = null; foreach (var searchinterface in typeof(T).GetInterfaces()) { if (searchinterface.Equals(typeof(IXMLSerializable))) { implementsInterface = true; } } if (implementsInterface) { //if it implements the interface, we'll use the GetXMLData. buildfunc = (elem) => ((IXMLPersistable)elem).GetXmlData(pNodeName); } else { //otherwise, let's see if there is an XMLProvider we can use. var retrievehelper = GetHelper<T>(); if (retrievehelper == null) { throw new ArgumentException("Provided type " + typeof(T).Name + " Does not implement IXMLSerializable and does not have a IXMLProvider implementation available."); } else { buildfunc = (elem) => retrievehelper.SerializeObject(elem, pNodeName); } } if (buildfunc == null) { return null; } return buildfunc(SourceData); } |
Essentially, if the type T is a class that implements the interface, it will use the interface method. Otherwise, it looks for a provider; if there is one present, it will use that provider’s SerializeObject interface method. otherwise, an ArgumentException is thrown.
I’ve been integrating this library into BASeBlock, by working from the bottom (top?) of the heirarchy from the least derived to the most derived, as well as adding any required providers along the way. With any luck using it this way may end up creating a “expected” XML Format for a LevelSet File, that would also make it (at least somewhat) user-editable, which would be quite cool.
Here is the source code for the library:XMLSerialization. If I get around to uploading it to a standard source control such as git I will try to remember to update this post.
Have something to say about this post? Comment!