65.9K
CodeProject 正在变化。 阅读更多。
Home

.NET XML 序列化 - 设置类

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.70/5 (36投票s)

2002年5月21日

BSD

8分钟阅读

viewsIcon

333268

downloadIcon

5685

一个可以用来在 XML 文档中存储值的设置类

Sample Image - xmlsettings.gif

引言

当我第一次和 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` 属性的产品的名称。

如何使用该类

  1. 修改此类以包含您自己的属性
    1. 向类中添加一个新的 `public get` / `set` 属性。
    2. 在 set 部分,使用属性名作为键将其设置到 `Hashtable` 中。
    3. 在 get 部分,使用属性名作为键返回存储在 `Hashtable` 中的值(您可能需要强制转换 `Hashtable` 中的值)。
  2. 测试以确保底层类型没有问题;如果没有问题,那么文章的其余部分只是供您消化信息。如果存在问题,请继续阅读以发现问题的原因以及如何解决它们。

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` 属性,但在远程处理方面您将一无所有。

考虑到对这些不足之处的解决方法并不难实现,我越来越赞同微软的决定。

一如既往,错误报告应发布在下方,评论或问题可以发布或发送电子邮件给我。

© . All rights reserved.