.NET Application Settings and IXmlSerializer

While working on the latest revision of OnTopReplica, I tought it would be great to store the custom regions selected by the user and persist them to file, in order to reload them when the application starts. So, the problem was essentially:

"How to store a list of rectangles (regions) and strings to file and load them again?"

Of course, working with .NET on Windows, you are confronted with several options for storing your application's data:

  • Local files: this is one of my favorite options, since the settings file can be copied and modified very easily by the user. However, on Vista this file would most likely be stored in a folder normally not accessible to a limited user account, and you'd have to write some kind of parsing and file handling functionality.
  • The register: this may sound as the place to store settings, but I really don't like the settings being hidden from the user's eyes. And by the way, I hate when uninstalled applications leave a mess behind in the register.
  • .NET Application Settings: this is the simplest approach and is recommended by Microsoft itself. The .NET component will store the data for you in a folder in the user's AppData files, loading and parsing it automagically. Additionally, Visual Studio has a settings editor that simplifies your task enormously.

Closer look to the .NET Settings

The Visual Studio project tree All Visual Studio C# projects include a Settings.settings file that can be used to store the application's data. The format of this data can be changed very easily using the visual editor and Visual Studio will automatically generate a specific class that loads, stores and handles your data and can be used from your code.

All data is usually serialized as an XML file in:

\Users\<User>\AppData\Local\<AppAuthor>\<AppName>_<Hash>

The built-in editor looks like this:

The settings editor

Every data element can be either a User element (which means it may assume a different value from user to user, on the same system) and an Application element (will have a unique value and be read-only). Accessing the data from code is really simple:

Intellisense support

Also remember that settings will be loaded automatically on start up, but you'll have to explicitly save the data to disk (usually whan you close the main form and exit the application):

Settings.Default.Save();

The problem: storing an ArrayList of objects

The code above works perfectly in most cases, is easy to use, shifts a lot of complexity out of your code... great!  :D

But in fact, after a couple of tests, I found out that I was unable to store a list of objects to the file. It seems to be impossible to use Generic collections in the NAP settings, therefore I had to choose a standard ArrayList of objects in order to store the user's regions (a Rectangle and a string, as said before). But even if I tried to save the ArrayList to disk it didn't work still, returning an empty collection of regions on the next start up...  :S

The problem is simple if you think about it: the settings file manager asks the ArrayList to serialize itself to Xml. That's easily done, but unfortunately ArrayList knows nothing about the stuff it contains and cannot serialize it at all! The solution I found is the following:
Implement a specific derivate of ArrayList with a custom serialization function and call the correct (custom) serialization method on the objects in the collection.

Sounds easy? It is (after you hit the wall a couple of times...): this is the code of the custom class deriving from ArrayList:

public class StoredRegionArray : ArrayList, IXmlSerializable { #region IXmlSerializable Members public System.Xml.Schema.XmlSchema GetSchema() { return null; } public void ReadXml(System.Xml.XmlReader reader) { this.Clear(); XmlSerializer x = new XmlSerializer(typeof(StoredRegion)); while (reader.ReadToFollowing("StoredRegion")) { object o = x.Deserialize(reader); if (o is StoredRegion) this.Add(o); } } public void WriteXml(System.Xml.XmlWriter writer) { XmlSerializer x = new XmlSerializer(typeof(StoredRegion)); foreach(StoredRegion sr in this){ x.Serialize(writer, sr); } } #endregion }

And the following is the "StoredRegion" class. This class simply keeps a rectangle and a string together and handles their XML serialization (in order to be serialized by the ArrayList above):

public class StoredRegion : IXmlSerializable { public StoredRegion() { } public StoredRegion(Rectangle r, string n) { Rect = r; Name = n; } public Rectangle Rect { get; set; } public string Name { get; set; } public override string ToString() { return Name; } #region IXmlSerializable Members public System.Xml.Schema.XmlSchema GetSchema() { return null; } public void ReadXml(System.Xml.XmlReader reader) { if (reader.MoveToAttribute("name")) Name = reader.Value; else throw new Exception(); reader.Read(); XmlSerializer x = new XmlSerializer(typeof(Rectangle)); Rect = (Rectangle)x.Deserialize(reader); } public void WriteXml(System.Xml.XmlWriter writer) { writer.WriteAttributeString("name", Name); XmlSerializer x = new XmlSerializer(typeof(Rectangle)); x.Serialize(writer, Rect); } #endregion }

And that's it! Selecting the StoredRegionArray as the type we wish to store in the settings editor, the list of regions will be correctly serialized and deserialized using our custom methods.  :)