![]() |
Languages »
XML »
Serializing
Intermediate
License: The BSD License
.NET XML Serialization - a settings classBy James T. JohnsonA settings class that can be used to store values in an XML document |
C#, XML.NET 1.0, Win2K, WinXP, Dev
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||

When I first teamed up with Christian the screensaver didn't have a very elegant way of persisting its settings. In the constructor for the screensaver form settings were loaded from the registry, and the same code was copied and pasted into the constructor for the Options dialog.
After my failure of trying to speed up the drawing process (curse you Microsoft for making CachedBitmap an
internal class), the first task I brought myself to was to try to clean up the settings code. What resulted is the
Settings class; which can be used for storing settings for the user or application.
This class differs slightly from the one in the screensaver because it has been refactored a bit but the premise behind it is the same.
Unless specified, members are assumed to be public instance.
string SettingsDirectory
ApplicationData directory for the user.void SaveSettingsToFile(Settings settings)void SaveSettingsToFile(string filename, Settings settings)
SettingsDirectory\config.datSettings LoadSettingsFromFile()Settings LoadSettingsFromFile(string filename)
SettingsDirectory\config.dat. If the file doesn't exist or there is an error deserializing the file it will
return a new Settings object with the default values.void LoadDefaultValues()
Hashtable settings
string representing
the property name. This makes updating the class fast and easy because you don't have to mess with private
variables.string companyName - (private, static)
SettingsDirectory property.string productName - (private, static)
SettingsDirectory property.Hashtable using the property name as a key.Hashtable using the property name as the
key (you will probably need to cast the value from the Hashtable)The XmlSerializer class does most of the work needed for saving the data in a class to a file. Unlike the other
serializers the XmlSerializer requires the classes it serializes to have a public default constructor (a public
constructor that takes no parameters) and it only serializes public properties and fields.
Because of this it isn't well suited for classes with lots of internal data, unless those can be completely rebuilt via public
properties/field. Because of this limitation, certain types won't be serialized by it, a couple examples are
System.Drawing.Color and System.Drawing.Font. I don't have a list of all the classes that fail so
you'll just have to use trial and error to find them, if serialization of the class isn't supported an exception will be thrown
when you try to create the XmlSerializer or the resulting serialization will be an empty tag. I describe a couple
techniques to work around this, and I provided fixex for both Color and Font classes so they work, with
little effort.
Serializing and deserializing a class with the XmlSerializer is really easy, because it is I'm not going to spend
much time on it; but instead I'll spend most of my time describing how to work around its limitations. Simply create a new
instance of the XmlSerializer class passing in the Type that corresponds to your class.
XmlSerializer xs = new XmlSerializer(typeof(Settings));
To serialize a class, call Serialize() and pass in a Stream and an instance of the class you wish to
serialize. To deserialize, call Deserialize() and pass in the Stream to read from, and cast the return
value back to your class type.
System.Drawing.Color is the first limitation I worked around. I did this by first deciding how I was going to
store the data in the XML file. I chose a string delimited by colons (:). This string is made up of two parts, the first part
tells what type of data follows, either a color name or ARGB values. The second part is either a name or the values split up by
colons.
First I created an enumeration that would signify each type.
public enum ColorFormat
{
NamedColor,
ARGBColor
}
Then with Color in hand I can reference the IsNamedColor property to decide which of the two formats
to return. I do that with this bit of code.
public string SerializeColor(Color color)
{
if( color.IsNamedColor )
return string.Format("{0}:{1}",
ColorFormat.NamedColor, color.Name);
else
return string.Format("{0}:{1}:{2}:{3}:{4}",
ColorFormat.ARGBColor,
color.A, color.R, color.G, color.B);
}
If you inspect the return value you'll see that what is written for the ColorFormat specifier is the name of the
enum value. This causes a slight problem, but is quickly remedied by the Enum class having a Parse
method which will convert the name back to a value.
Speaking of reading the values back, here is how I did it. First I take the string in and split it up using colon as the
separation character. This results in a string array with 2 or 5 elements in it. I then take the first element and run it through
Enum's Parse() method to convert it back to a value suitable for the enum. Then I check that value to
determine which of the two formats it is.
public Color DeserializeColor(string color)
{
byte a, r, g, b;
string [] pieces = color.Split(new char[] {':'});
ColorFormat colorType = (ColorFormat)
Enum.Parse(typeof(ColorFormat), pieces[0], true);
switch(colorType)
{
case ColorFormat.NamedColor:
return Color.FromName(pieces[1]);
case ColorFormat.ARGBColor:
a = byte.Parse(pieces[1]);
r = byte.Parse(pieces[2]);
g = byte.Parse(pieces[3]);
b = byte.Parse(pieces[4]);
return Color.FromArgb(a, r, g, b);
}
return Color.Empty;
}
With that code it is now possible serialize a Color object as a string; which the
XmlSerializer will handle. How does one go about doing that, and make the resulting XML look as though it handled it
natively? By using two attributes you can change the name an element will have in the XML document and tell it ignore other
elements as well.
The attribute XmlIgnoreAttribute when applied to a property or field tells the XmlSerializer to
ignore that field/property when serializing the class. The attribute XmlElementAttribute does various functions, but
the one we're interested in is renaming a serialized element to something else. For our purposes we will apply the
XmlIgnore attribute to the original property which can't be serialized; then we'll apply the
XmlAttribute to the XmlSerializer friendly property to rename it something more suitable.
[XmlIgnore()]
public Color ColorType
{
get
{
return (Color) settings["color"];
}
set
{
settings["color"] = value;
}
}
[XmlElement("ColorType")]
public string XmlColorType
{
get
{
return Settings.SerializeColor(ColorType);
}
set
{
ColorType = Settings.DeserializeColor(value);
}
}
Here you see that I have a public property named ColorType that will return the Color stored in the
settings object. There is also a string property that is used by the XmlSerializer to store the
underlying value.
ColorType is the property that the user of the class is to use for setting/retrieving properties.
XmlColorType is used by the XmlSerializer to get/set the underlying value and should not be used by the
programmer.
Another way to work around the limitation is to create a class that exposes only the properties needed to recreate the object, and use that to serialize.
This is what I did for the Font class; exposing the FontFamily, Size,
FontStyle, and the GraphicsUnit associated with a Font instance. In keeping with the
pattern I used for Color; I supply a property that uses this class, and I have two protected methods which handle
moving back and forth between the two types.
public struct XmlFont
{
public string FontFamily;
public GraphicsUnit GraphicsUnit;
public float Size;
public FontStyle Style;
public XmlFont(Font f)
{
FontFamily = f.FontFamily.Name;
GraphicsUnit = f.Unit;
Size = f.Size;
Style = f.Style;
}
public Font ToFont()
{
return new Font(FontFamily, Size, Style,
GraphicsUnit);
}
}
This is an extremely simple struct. Its entire purpose is to be a lightweight container for the values that will be persisted to the file I don't bother defining properties, and I made it a struct instead of a class so it *is* lightweight. Depending on your needs you could use a heavier implementation; but for this case this works great.
This works because the XmlSerializer will attempt to serialize every property and field, if the property or field
is a simple datatype it puts it inline, for structs and classes it serializes its public properties/fields.
I've often wondered why it is that MS didn't let the XmlSerializer work like the Formatters,
serializing both public and private data. After toying around with the Settings class a bit I think I discovered
why. There isn't a need! The XmlSerializer was made to persist data in a user-readable way, but still be read easily
by a program. Publishing private data would be considered a Bad Thing� and thus would make life difficult for those trying to
keep private data private. Though you could go through and add NonSerializable attributes to all your private data,
you would be SOL when it came to remoting.
Considering that the workaround for the deficiencies isn't all that difficult to get working I'm coming to agree with Microsoft's decision.
As always bug reports should be posted below, comments or questions can be posted or e-mailed to me.
If you want the XP theme I used in the screenshot, check out the WaterColor Visual Style from ThemeXP.org.
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 20 May 2002 Editor: James T. Johnson |
Copyright 2002 by James T. Johnson Everything else Copyright © CodeProject, 1999-2009 Web10 | Advertise on the Code Project |