.NET XML 序列化 - 设置类






4.70/5 (36投票s)
一个可以用来在 XML 文档中存储值的设置类
引言
当我第一次和 Christian 合作时,屏幕保护程序并没有一种非常优雅的方式来持久化其设置。在屏幕保护程序窗体的构造函数中,设置从注册表中加载,然后相同的代码被复制并粘贴到 Options 对话框的构造函数中。
在我尝试加速绘图过程失败后(该死的 Microsoft,让 `CachedBitmap` 成为了一个内部类),我首先要做的就是清理设置代码。最终产生了 `Settings` 类;它可以用于存储用户或应用程序的设置。
此类与屏幕保护程序中的类略有不同,因为它被重构了一些,但其基本原理是相同的。
类的组成
除非另有说明,成员假定为 `public` 实例。
- 属性
string SettingsDirectory
- (`static`) 如果 Save/Load 函数没有给出文件名,设置将保存在这里。此目录是用户的默认 `ApplicationData` 目录。
- 方法
void SaveSettingsToFile(Settings settings)
void SaveSettingsToFile(string filename, Settings settings)
- (`static`) 将设置保存到文件中,如果未指定文件名,则默认为 *SettingsDirectory\config.dat*。
Settings LoadSettingsFromFile()
Settings LoadSettingsFromFile(string filename)
- (`static`) 从文件中加载设置,如果未指定文件名,则默认为 *SettingsDirectory\config.dat*。如果文件不存在或反序列化文件时出错,将返回一个带有默认值的新的 `Settings` 对象。
void LoadDefaultValues()
- 将默认设置加载到属性中。
- 字段
Hashtable settings
- (`protected`) 用于存储实际设置,基于键;键应该是代表属性名的 `string`。这使得更新类变得快速简便,因为您不必处理 `private` 变量。
- `string companyName` - (`private`, `static`)
- 包含将用于创建 `SettingsDirectory` 属性的公司名称。
- `string productName` - (`private`, `static`)
- 包含将用于创建 `SettingsDirectory` 属性的产品的名称。
如何使用该类
- 修改此类以包含您自己的属性
- 向类中添加一个新的 `public get` / `set` 属性。
- 在 set 部分,使用属性名作为键将其设置到 `Hashtable` 中。
- 在 get 部分,使用属性名作为键返回存储在 `Hashtable` 中的值(您可能需要强制转换 `Hashtable` 中的值)。
- 测试以确保底层类型没有问题;如果没有问题,那么文章的其余部分只是供您消化信息。如果存在问题,请继续阅读以发现问题的原因以及如何解决它们。
XmlSerializer 类
`XmlSerializer` 类完成了将类数据保存到文件所需的大部分工作。与其他序列化器不同,`XmlSerializer` 要求它序列化的类具有 `public` 的默认构造函数(一个不接受任何参数的 `public` 构造函数),并且它只序列化 `public` 属性和字段。
因此,它不适合拥有大量内部数据的类,除非这些数据可以通过 `public` 属性/字段完全重建。由于这个限制,某些类型将不会被它序列化,例如 `System.Drawing.Color` 和 `System.Drawing.Font`。我没有所有失败类的列表,所以您只能通过试错来查找它们,如果类的序列化不受支持,则在尝试创建 `XmlSerializer` 时会抛出异常,或者生成的序列化结果将是一个空标签。我描述了几种解决方法,并为 `Color` 和 `Font` 类提供了修复方案,使它们能够轻松工作。
使用 `XmlSerializer` 序列化和反序列化一个类非常容易,因此我不会花太多时间介绍它;相反,我将花费大部分时间描述如何解决它的限制。只需创建一个 `XmlSerializer` 类的新实例,传入与您的类对应的 `Type`。
XmlSerializer xs = new XmlSerializer(typeof(Settings));
要序列化一个类,请调用 `Serialize()` 并传入一个 `Stream` 和您要序列化的类的实例。要反序列化,请调用 `Deserialize()` 并传入要读取的 `Stream`,然后将返回值转换回您的类类型。
绕过限制
自定义解析
`System.Drawing.Color` 是我第一个解决的限制。我首先决定如何在 XML 文件中存储数据。我选择了一个用冒号(`:`)分隔的 `string`。这个 `string` 由两部分组成,第一部分指示后面是什么类型的数据,要么是颜色名称,要么是 ARGB 值。第二部分是名称或以冒号分隔的值。
首先,我创建了一个枚举来表示每种类型。
public enum ColorFormat
{
NamedColor,
ARGBColor
}
然后,有了 `Color` 对象,我就可以引用 `IsNamedColor` 属性来决定返回哪种格式。我通过这段代码来实现。
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);
}
如果您检查返回值,您会看到 `ColorFormat` 指定符写入的是 `enum` 值的名称。这会产生一个小问题,但 `Enum` 类有一个 `Parse` 方法,可以将名称转换回一个值,从而很快得到解决。
说到读取值,我就是这样做的。首先,我将 `string` 输入并使用冒号作为分隔符将其拆分。这会产生一个包含 2 或 5 个元素的 `string` 数组。然后,我将第一个元素通过 `Enum` 的 `Parse()` 方法进行转换,将其转换回适合 `enum` 的值。然后我检查该值以确定它是哪种格式。
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;
}
有了这些代码,现在就可以将 `Color` 对象序列化为 `string` 了;`XmlSerializer` 将会处理它。如何做到这一点,并使生成的 XML 看起来好像是原生处理的一样?通过使用两个属性,您可以更改 XML 文档中元素的名称,并告诉它忽略其他元素。
`XmlIgnoreAttribute` 属性应用于属性或字段时,会告诉 `XmlSerializer` 在序列化类时忽略该字段/属性。`XmlElementAttribute` 属性有多种功能,但我们感兴趣的是将一个序列化的元素重命名为其他名称。对于我们的目的,我们将 `XmlIgnore` 属性应用于无法序列化的原始属性;然后我们将 `XmlAttribute` 应用于 `XmlSerializer` 友好的属性,将其重命名为更合适的名称。
一个小例子
[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);
}
}
在这里,您可以看到我有一个名为 `ColorType` 的 `public` 属性,它将返回存储在设置对象中的 `Color`。还有一个 `string` 属性,供 `XmlSerializer` 用于存储底层值。
`ColorType` 是类用户用于设置/检索属性的属性。`XmlColorType` 由 `XmlSerializer` 用于获取/设置底层值,程序员不应使用它。
包装类/结构
另一种绕过限制的方法是创建一个类,该类仅公开用于重新创建对象所需的属性,并使用该类进行序列化。
这就是我为 `Font` 类所做的;公开与 `Font` 实例相关的 `FontFamily`、`Size`、`FontStyle` 和 `GraphicsUnit`。为了保持与 `Color` 相同的模式;我提供了一个使用此类属性,并且我有两个 `protected` 方法来处理两种类型之间的来回转换。
XmlFont 结构
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);
}
}
这是一个极其简单的 `struct`。它的全部目的是成为一个轻量级的容器,用于持久化到文件中的值。我不费心定义属性,并且我将其设置为 `struct` 而不是类,这样它*就*是轻量级的。根据您的需要,您可以使用更重的实现;但在此情况下,这效果很好。
这样做是因为 `XmlSerializer` 会尝试序列化每个属性和字段,如果属性或字段是简单的 `datatype`,它会将其内联;对于 `struct` 和类,它会序列化其 `public` 属性/字段。
结论
我经常想知道为什么 MS 不让 `XmlSerializer` 像 `Formatter` 那样工作,序列化 `public` 和 `private` 数据。在对 `Settings` 类进行了一些研究后,我想我发现了原因。没有必要!`XmlSerializer` 的目的是以用户可读的方式持久化数据,但仍能被程序轻松读取。发布 `private` 数据将被视为一件坏事™,因此会给那些试图保持 `private` 数据私有的人带来困难。虽然您可以遍历并为所有 `private` 数据添加 `NonSerializable` 属性,但在远程处理方面您将一无所有。
考虑到对这些不足之处的解决方法并不难实现,我越来越赞同微软的决定。
一如既往,错误报告应发布在下方,评论或问题可以发布或发送电子邮件给我。