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

通用数据点系列 XML 格式及其使用 LINQ to XML 进行的验证加载

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (3投票s)

2009年4月8日

CPOL

13分钟阅读

viewsIcon

25194

downloadIcon

117

如何以 XML 格式表示一系列通用数据点,并轻松读取它们。

前言

通常希望以某种 XML 格式提供通用数据点系列数据。XML 能够用属性装饰系列、将它们嵌套在复杂数据对象中、在一个数据文件中混合不同的数据系列/数据对象,并用简洁的 LINQ to XML 代码加载它们。

通用数据点是一种结构,只有两个必需的 {X, Y} 属性,用于表示二维空间中的点位置。每个点维度都有自己的“基本类型”,例如,数值、DateTime 等。这就是为什么使用“通用”一词。数据点对象类型由这些基本类型的对定义。我们可以用 XML 表示很多种点类型。例如,基本的数据点类型集由所有 XML 原子类型的笛卡尔自乘积产生。这个基本集可以通过包含 XPath 数据类型、通过限制派生的简单类型等来扩展。

数据点系列包含一个或多个相同类型的数据点。数据点系列类型由它包含的数据点的类型定义。

一个数据点系列 XML 文档可以包含多个不同类型的数据点系列。每个应用程序都可以根据自己的要求来接受或禁止特定类型的数据点系列。Loader 库必须能够根据以下要求列表验证文件格式

  1. 确保数据点系列中的所有数据点类型相同。
  2. 限制它包含的数据点系列类型列表。
  3. 根据 XSD 命名空间以及可选的其他命名空间(如 XPath),其中定义了基本类型,来验证数据点维度的值。
  4. 更多...

其中一些要求是应用程序特定的,因此应用程序必须以某种方式向 Loader 类提供相应的信息。

我们将使用 XML 模式来验证数据点系列 XML 文档的内容。这种方法提供了以下机会

  1. Loader 代码与可以通过 XML 模式描述的功能分离。这使得 Loader 代码既通用又简洁。我们将使用 LINQ to XML 加载数据。
  2. 以某种形式将 XML 模式数据传递给 Loader 类。Loader 类可以使用
    1. 库中存储的默认模式。这是易于使用的选项,但缺乏可配置性和可扩展性。
    2. 基于类型映射的动态生成模式(下方有详细信息)。此选项允许定义预期的所有数据点系列类型列表,但(目前)将基本类型列表限制为 XML 模式的原子类型。
    3. 用户提供的模式。这是最强大的选项,但用户应了解 XML 模式语言。

通用数据点系列 XML 格式

首先,我们必须定义根元素。假设它称为 Items。为了安全起见,我们将要求它定义默认 XML 命名空间 urn:PointSeries-schema。根元素将如下所示

<?xml version="1.0" encoding="utf-8"?>
<Items xmlns="urn:PointSeries-schema">
...
</Items>

根元素包含无限制数量的数据点系列。首先,我们将尝试定义点系列元素如下

<Items xmlns="urn:PointSeries-schema">
  <Points ...>
  </Points>
  <Points ...>
  </Points>
  ...
</Items>

这行不通,因为不同的数据点系列元素可能包含不同基本类型(Base Type)的点,因此数据点系列元素本身也可能是不同类型的。XML 模式规则不允许在同一作用域内具有相同名称的不同类型的元素。因此,我们必须为不同类型的数据点系列元素分配不同的名称。

因此,我们根据以下模式为数据点系列元素命名

  • 如果两个数据系列维度具有相同的基本类型,则为 <Points.BaseType ...>。例如,<Points.Double ...>
  • 如果数据系列维度具有不同的基本类型,则为 <Points.XBaseType.YBaseType ...>。例如,<Points.DateTime.Int ...>

BaseTypeXBaseTypeYBaseType 数据点系列元素名称部分统称为“类型字符串”。有必要就如何定义这些类型字符串达成一致,并建立类型字符串、XSD 定义的数据类型和 CLR 数据类型之间的映射。

表 1. XSD 类型到 CLR 类型到类型字符串的映射示例
XSD 类型 描述 示例 类型字符串 .NET 类型
xsd:int 一个整数,可以表示为四字节、二进制补码数 -2147483648, 2147483645,..., -3, -2, -1, 0, 1, 2, 3, ... Int System.Int32
xsd:double IEEE 754 64 位浮点数 -INF, 1.401E-90, -1E4, -0, 0, 12.78E-2, 12, INF, NaN, 3.4E42 双精度浮点型 System.Double
xsd:dateTime 协调世界时 (UTC) 的特定时刻,精度可任意小 1999-05-31T13:20:00.000-05:00, 1999-05-31T18:20:00.000Z, 1999-05-31T13:20:00.000, 1999-05-31T13:20:00.000-05:32 日期时间 System.DateTime
xsd:date 历史上的特定日期 0044-03-15, 0001-01-01, 1969-06-27, 2000-10-31, 2001-11-17 日期 System.DateTime
xsd:gMonth 某年中的某个月 --01--, --02--, --03--,..., --09--, --10--, --11--, --12-- System.Int32

此表包含 XSD 简单类型的列表。您可以通过包含其他 XML 类型来扩展它。

根据上面的映射,例如,<Points.Double ...> 数据点系列 XML 元素应包含 x 和 y 维度均为 xsd:double 类型的点,并且这些点将被加载为具有 System.Double x、y 属性的点。

Point 元素本身类似于 <Point x="2008-01-01" y="-20"/>,带有必需的 x 和 y 属性。

下面是示例输入 XML 数据文件中的摘录

<?xml version="1.0" encoding="utf-8"?>
<Items xmlns="urn:PointSeries-schema">
  <Points.Int.Double YName="y=x^2">
    <Point x="0" y="0"/>
    <Point x="1" y="0.01"/>
    ...
  </Points.Int.Double>
  <Points.Date.Int YName="temperature" XName="Date">
    <Point x="2008-01-01" y="-20"/>
    <Point x="2008-02-01" y="-25"/>
    ...
  </Points.Date.Int>
  <Points.Month.Double YName="2008 year month temperatures" XName="Month">
    <Point x="--01--" y="-20.8"/>
    <Point x="--02--" y="-25.2"/>
    ...
  </Points.Month.Double>
  ...
</Items>

注意:点系列元素用可选的 YNameXName 属性装饰,用于表示 x 和 y 维度标签。

XML架构

通用数据点系列 XML 格式由 XML 模式定义,其摘录如下

<?xml version="1.0" encoding="utf-8"?>
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" 
           xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <!-- Root element -->
  <xs:element name="Items" type="itemsType"/>
  
  <!-- Root element type -->
  <xs:complexType name="itemsType">
    <xs:choice maxOccurs="unbounded">
      <xs:element name="Points.Int" type="pointsIntIntType"/>
      <xs:element name="Points.Int.DateTime" type="pointsIntDttmType"/>
      ...
      <xs:element name="Points.Double" type="pointsDblDblType"/>
      <xs:element name="Points.Double.Int" type="pointsDblIntType"/>
      ...
    </xs:choice>
  </xs:complexType>
  
  <!-- Point Series Type attributes -->
  <xs:attributeGroup name="pointSetAttributes">
    <xs:attribute name="YName" 
      type="xs:string" use="optional" />
    <xs:attribute name="XName" 
      type="xs:string" use="optional" />
  </xs:attributeGroup>

  <!-- Point Series Types -->
  <xs:complexType name="pointsIntIntType">
      <xs:sequence>
        <xs:element minOccurs="1" 
            maxOccurs="unbounded" name="Point">
          <xs:complexType>
            <xs:attribute name="x" 
              type="xs:int" use="required" />
            <xs:attribute name="y" 
              type="xs:int" use="required" />
          </xs:complexType>
        </xs:element>
      </xs:sequence>
    <xs:attributeGroup ref="pointSetAttributes"/>
  </xs:complexType>
  <xs:complexType name="pointsIntDttmType">
      <xs:sequence>
        <xs:element minOccurs="1" 
                  maxOccurs="unbounded" name="Point">
          <xs:complexType>
            <xs:attribute name="x" 
                     type="xs:int" use="required" />
            <xs:attribute name="y"
                     type="xs:dateTime" use="required" />
          </xs:complexType>
        </xs:element>
      </xs:sequence>
    <xs:attributeGroup ref="pointSetAttributes"/>
  </xs:complexType>
  ...
  <xs:complexType name="pointsDblIntType">
      <xs:sequence>
        <xs:element minOccurs="1" 
              maxOccurs="unbounded" name="Point">
          <xs:complexType>
            <xs:attribute name="x" 
                 type="xs:double" use="required" />
            <xs:attribute name="y" 
                 type="xs:int" use="required" />
          </xs:complexType>
        </xs:element>
      </xs:sequence>
    <xs:attributeGroup ref="pointSetAttributes"/>
  </xs:complexType>
  <xs:complexType name="pointsDblDblType">
      <xs:sequence>
        <xs:element minOccurs="1" 
                 maxOccurs="unbounded" name="Point">
          <xs:complexType>
            <xs:attribute name="x" 
                 type="xs:double" use="required" />
            <xs:attribute name="y" 
                 type="xs:double" use="required" />
          </xs:complexType>
        </xs:element>
      </xs:sequence>
    <xs:attributeGroup ref="pointSetAttributes"/>
  </xs:complexType>
  ...
</xs:schema>

此模式定义了 <Items ...> 根元素,其预期内容由 XSD choice 选择器定义。您应该修改选择器内容,只包含您的应用程序期望的数据点系列元素类型。

模式的其余部分包含大量的元素类型定义。这些类型中的每一种都定义了具有特定 x、y 基本类型的点系列。

您可以使用 XML Schema 类型派生规则在模式中定义新的基本类型。

类型映射

手动编写或编辑数据点系列 XML 模式很繁琐,并且需要了解 XML 模式规范(请参阅 part1part2)。

相反,模式可以即时组合。如果您查看上面的模式摘录,您会发现大部分文本在一种类型定义到另一种类型定义之间是重复的。从一种模式到另一种模式变化的信息可以用比模式本身更紧凑的形式来表示。组成模式所需的一切数据就像 表 1 中的那样。我们应该通过定义基本类型以及 XSD 和 CLR 类型之间的映射以及用于构建数据点系列 XML 元素标签名称的“类型字符串”来描述数据点系列类型。

这是一个示例类型映射 XML 文档摘录

<?xml version="1.0" encoding="utf-8"?>
<Mappings xmlns="urn:PointSeries-mapping">
  <Mapping>
    <XAxis xsd-type="int" clr-type="System.Int32" type-string="Int"/>
    <YAxis xsd-type="double" type-string="Double"/>
  </Mapping>
  <Mapping>
    <XAxis xsd-type="double" clr-type="System.Double" type-string="Double"/>
    <YAxis xsd-type="date" clr-type="System.DateTime" type-string="Date"/>
  </Mapping>
  <Mapping>
    <XAxis xsd-type="double" clr-type="System.Double" type-string="Double"/>
    <YAxis xsd-type="gMonth" clr-type="System.Int32" type-string="Month"/>
  </Mapping>
  ...
  <Mapping>
    <XAxis xsd-type="dateTime" type-string="DateTime"/>
    <YAxis xsd-type="double" type-string="Double"/>
  </Mapping>
</Mappings>

Mappings 元素声明了 urn:PointSeries-mapping XML 命名空间。它可以包含一个或多个 Mapping 元素。

Mapping 元素定义了一个数据点系列类型。它包含两个元素:XAxis 用于 x 维度,YAxis 用于 y 维度。

每个 ...Axis 元素在 XML 世界(xsd-type)和 .NET 世界(clr-type)中定义了维度的类型。type-string 属性提供了用于组合数据 XML 文件中的数据点系列元素名称的名称。例如,上面代码片段中的第一个映射元素将为 <Points.Int.Double> 元素生成类型定义。xsd-typetype-string 属性是必需的,而 clr-type 属性是可选的。如果省略,则 CLR 类型将从硬编码在 Loader 库中的 XSD 类型到 CLR 类型的默认映射表中推断(这与 .NET 使用的映射相同,请参阅 将 XML 数据类型映射到 CLR 类型)。如果存在,则 Loader 将尝试将 XSD 类型的值转换为指定的 CLR 类型。例如,请参阅第三个 Mapping 元素。gMonth XSD 类型的默认 CLR 类型是 DateTime,但 clr-type 属性值是 Int32。Loader 将借助 XML 转换器类实例,将 gMonth 类型的值转换为 Int32,如下所示。请注意,clr-type 属性值可以包含完整的程序集限定类型名称。

映射文件不得包含矛盾的条目:它不得定义两个具有相同元素名称的数据点系列元素。

映射文档根据以下模式进行验证

<?xml version="1.0" encoding="utf-8"?>
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" 
           targetNamespace="urn:PointSeries-mapping"
           xmlns="urn:PointSeries-mapping"
           xmlns:xs="http://www.w3.org/2001/XMLSchema">
  
  <xs:element name="Mappings">
    <xs:complexType>
      <xs:sequence>
        <xs:element maxOccurs="unbounded" name="Mapping" type="mappingType"/>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
  
  <xs:complexType name="axisType">
    <xs:attribute name="xsd-type" type="xs:string" use="required" />
    <xs:attribute name="clr-type" type="xs:string" use="optional" />
    <xs:attribute name="type-string" type="xs:string" use="required" />
  </xs:complexType>
  
  <xs:complexType name="mappingType">
    <xs:sequence>
      <xs:element name="XAxis" type="axisType"/>
      <xs:element name="YAxis" type="axisType"/>
    </xs:sequence>
  </xs:complexType>
</xs:schema>

此模式作为资源存储在 Loader 库程序集中。

读取数据

在文章附带的代码中,所有数据读取代码都放在 Loader 类库项目(生成 XmlDataPointSeries.Loader 程序集)中。Loader 类包含数据读取/解析代码,而补充类 XsdDataPointDataPointDataPointSeries 提供了存储结果的位置。

/// <summary>
/// Loads <see cref="DataPointSeries"/> collection from an XML file or Stream.
/// </summary>
/// <remarks>
/// Contents of the Data Point Series XML document is validated against a XML schema. 
/// <para>That schema is either
/// <list type="number">
/// <item>Prebuilt and stored as the resource.</item>
/// <item>Provided by the user.</item>
/// <item>Dynamically constructed from the contents of XML mapping data.</item>
/// </list>
/// </para>
/// </remarks>
public static class Loader
{
    // XML namespace must be used in XML data files.
    internal const string dataNamespaceName = "urn:PointSeries-schema";

    #region LoadWithSchema
    /// <summary>
    /// Loads a <see cref="DataPointSeries"/> collection from
    /// the <paramref name="dataReader"/> specified with
    /// the XML schema provided by <paramref name="schemaReader"/>.
    /// </summary>
    /// <param name="dataReader">XML DataPointSeries collection
    /// <see cref="System.IO.TextReader"/>.</param>
    /// <param name="schemaReader">XML schema <see cref="System.IO.TextReader"/>.</param>
    /// <returns><see cref="DataPointSeries"/> collection.</returns>
    /// <exception cref="ValidationException"/>
    public static IEnumerable<DataPointSeries> 
           LoadWithSchema(TextReader dataReader, XmlReader schemaReader){ ... }
    /// <summary>
    /// Loads <see cref="DataPointSeries"/> collection from
    /// the <paramref name="dataStream"/> specified with
    /// the XML schema provided by <paramref name="schemaStream"/>.
    /// </summary>
    /// <param name="dataStream">Input XML data <see cref="System.IO.Stream"/>.</param>
    /// <param name="schemaStream">Input XML schema <see cref="System.IO.Stream"/>.</param>
    /// <returns><see cref="DataPointSeries"/> collection.</returns>
    /// <exception cref="ValidationException"/>
    public static IEnumerable<DataPointSeries> 
           LoadWithSchema(Stream dataStream, Stream schemaStream) { ... }
    /// <summary>
    /// Loads <see cref="DataPointSeries"/> collection from the
    /// <paramref name="dataFileName"/> file specified with
    /// the XML schema provided by <paramref name="schemaFileName"/>.
    /// </summary>
    /// <param name="dataFileName">Name of the data file.</param>
    /// <param name="schemaFileName">Name of the schema file.</param>
    /// <returns><see cref="DataPointSeries"/> collection.</returns>
    /// <exception cref="ValidationException"/>
    public static IEnumerable<DataPointSeries> 
           LoadWithSchema(string dataFileName, string schemaFileName) { ... }
    /// <summary>
    /// Loads a <see cref="DataPointSeries"/> collection from
    /// the XML data <paramref name="dataReader"/>
    /// specified with prebuilt XML schema.
    /// </summary>
    /// <param name="dataReader">DataPointSeries collection
    /// XML <see cref="System.IO.TextReader"/>.</param>
    /// <returns><see cref="DataPointSeries"/> collection.</returns>
    /// <exception cref="ValidationException"/>
    public static IEnumerable<DataPointSeries> LoadWithSchema(TextReader dataReader) { ... }
    /// <summary>
    /// Loads a <see cref="DataPointSeries"/> collection
    /// from the XML data file specified with prebuilt XML schema.
    /// </summary>
    /// <param name="dataStream">Input XML data <see cref="System.IO.Stream"/>.</param>
    /// <returns><see cref="DataPointSeries"/> collection.</returns>
    /// <exception cref="ValidationException"/>
    public static IEnumerable<DataPointSeries> LoadWithSchema(Stream dataStream) { ... }
    /// <summary>
    /// Loads a <see cref="DataPointSeries"/> collection
    /// from the XML data file specified with the prebuilt XML schema.
    /// </summary>
    /// <param name="fileName">DataPointSeries collection XML file Name.</param>
    /// <returns><see cref="DataPointSeries"/> collection.</returns>
    /// <exception cref="ValidationException"/>
    public static IEnumerable<DataPointSeries> LoadWithSchema(string fileName) { ... }
    #endregion LoadWithSchema

    #region LoadWithMappings
    /// <summary>
    /// Loads a <see cref="DataPointSeries"/> collection
    /// from the <paramref name="dataReader"/>
    /// specified with the mappings provided by the <paramref name="mappingReader"/>.
    /// </summary>
    /// <param name="dataReader">Input XML data
    /// <see cref="System.IO.TextReader"/>.</param>
    /// <param name="mappingReader">Input XML mapping
    /// <see cref="System.IO.TextReader"/>.</param>
    /// <returns><see cref="DataPointSeries"/> collection.</returns>
    /// <exception cref="ValidationException"/>
    public static IEnumerable<DataPointSeries> 
           LoadWithMappings(TextReader dataReader, TextReader mappingReader) { ... }
    /// <summary>
    /// Loads <see cref="DataPointSeries"/> collection from the 
    /// <paramref name="dataStream"/> specified.
    /// </summary>
    /// <param name="dataStream">Input XML data
    /// <see cref="System.IO.Stream"/>.</param>
    /// <param name="mappingStream">Input XML mapping
    /// <see cref="System.IO.Stream"/>.</param>
    /// <returns><see cref="DataPointSeries"/> collection.</returns>
    /// <exception cref="ValidationException"/>
    public static IEnumerable<DataPointSeries> 
           LoadWithMappings(Stream dataStream, Stream mappingStream) { ... }
    /// <summary>
    /// Loads <see cref="DataPointSeries"/> collection from the file specified.
    /// </summary>
    /// <param name="dataFileName">Name of the data file.</param>
    /// <param name="mappingFileName">Name of the mapping file.</param>
    /// <returns><see cref="DataPointSeries"/> collection.</returns>
    /// <exception cref="ValidationException"/>
    public static IEnumerable<DataPointSeries> 
           LoadWithMappings(string dataFileName, string mappingFileName) { ... }
    #endregion LoadWithMappings

    /// <summary>
    /// Parses the point series element tag and returns x,y type strings.
    /// </summary>
    /// <param name="tagName">Tag name.</param>
    /// <param name="xType">Output Type of the x-dimension.</param>
    /// <param name="yType">Output Type of the y-dimension.</param>
    static void getXYTypeStrings(string tagName, out string xType, out string yType)
    {
        int n = tagName.IndexOf('}');
        Debug.Assert(n > 0, "n > 0");
        const string pointsTagPrefix = "Points";
        int pointsTagPrefixLength = pointsTagPrefix.Length;
        Debug.Assert(tagName.Length > n + pointsTagPrefixLength + 1, 
                     "tagName.Length > n + pointsTagPrefixLength + 1");
        string xyTypes = tagName.Substring(n + pointsTagPrefixLength + 2);
        n = xyTypes.IndexOf('.');
        if (n < 0)
        {
            xType = xyTypes;
            yType = xyTypes;
        }
        else
        {
            xType = xyTypes.Substring(0, n);
            yType = xyTypes.Substring(n + 1);
        }
    }
}

此类提供了 LoadWithSchemaLoadWithMappings 方法重载,用于加载数据点系列 XML 文档,该文档要么根据默认模式、用户提供的模式进行验证,要么根据动态生成的模式进行验证。

根据设计,LoadWithSchemaLoadWithMappings 方法在文件打开、读取、解析和验证过程中发生的任何错误时都会失败,并抛出 System.IO 异常或包含错误描述的 LoaderValidationException。所有验证错误都由 ValidationException.ValidationErrors 属性返回;这使用户有机会一次性修复所有错误。

使用模式加载

主要的 LoadWithSchema 方法重载是

public static IEnumerable<DataPointSeries> 
       LoadWithSchema(TextReader dataReader, XmlReader schemaReader)
{
    StringBuilder sbErrors = null;
    List<ValidationException.ValidationError> errors = null;
    // Load and validate the schema.
    XmlSchema schema = XmlSchema.Read(schemaReader, (sender, e) =>
    {
        if (sbErrors == null)
            sbErrors = new StringBuilder();
        sbErrors.AppendFormat(
            "Schema validation error: {1}{0}Line={2}, position={3}{0}", 
            System.Environment.NewLine, e.Exception.Message, 
            e.Exception.LineNumber, e.Exception.LinePosition);
        if (errors == null)
            errors = new List<ValidationException.ValidationError>();
        errors.Add(new ValidationException.ValidationError()
        {
            Message = e.Exception.Message,
            Line = e.Exception.LineNumber,
            Position = e.Exception.LinePosition
        });
    });
    if (sbErrors != null)
        // Validation error(s) occured.
        throw new ValidationException(sbErrors.ToString(), errors.ToArray());
    XmlSchemaSet schemaSet = new XmlSchemaSet();
    schemaSet.Add(schema);

    // Load and validate the data file.
    using (XmlReader reader = XmlReader.Create(dataReader))
    {
        XDocument doc = XDocument.Load(reader);
        doc.Validate(schemaSet, (sender, e) =>
        {
            if (sbErrors == null)
                sbErrors = new StringBuilder();
            sbErrors.AppendFormat("Validation error: {1}{0}Line={2}, position={3}{0}"
                , System.Environment.NewLine, e.Exception.Message
                , e.Exception.LineNumber, e.Exception.LinePosition);
            if (errors == null)
                errors = new List<ValidationException.ValidationError>();
            errors.Add(new ValidationException.ValidationError()
            {
                Message = e.Exception.Message,
                Line = e.Exception.LineNumber,
                Position = e.Exception.LinePosition
            });
        }, true); 
        if (sbErrors != null)
            // Validation error(s) occured.
            throw new ValidationException(sbErrors.ToString(), errors.ToArray());

        XNamespace xns = dataNamespaceName;
        XElement items = doc.Element(xns + "Items");
        // Check the root element name (i.e. Items in "urn:PointSeries-schema" xmlns).
        //if (items.Name != xns + "Items")
        if (items == null)
            throw new ValidationException(string.Format("Root element {0} missed", 
                                                        xns + "Items"));
        // Parse the Point.XXX elements.
        return items.Elements().Select<XElement, DataPointSeries>(
            (item) =>
            {
                // Parse item tag name for X/Y type strings.
                string xType, yType;
                getXYTypeStrings(item.Name.ToString(), out xType, out yType);
                // Optional attributes.
                var yName = item.Attribute("YName");
                var xName = item.Attribute("XName");

                IXmlSchemaInfo schemaInfo = item.GetSchemaInfo();
                XmlSchemaElement e = schemaInfo.SchemaElement;

                DataPointSeries series = new DataPointSeries()
                {
                    XName = xName == null ? "" : xName.Value,
                    XTypeString = xType,
                    YName = yName == null ? "" : yName.Value,
                    YTypeString = yType
                };
                foreach (var pt in from pt in item.Elements(xns + "Point") select pt)
                {
                    XAttribute xAttr = pt.Attribute("x");
                    if (series.XXsdTypeString == null)
                        series.XXsdTypeString = 
                          xAttr.GetSchemaInfo().SchemaAttribute.SchemaTypeName.Name;
                    XAttribute yAttr = pt.Attribute("y");
                    if (series.YXsdTypeString == null)
                        series.YXsdTypeString = 
                          yAttr.GetSchemaInfo().SchemaAttribute.SchemaTypeName.Name;
                    series.XsdPoints.Add(new XsdDataPoint((string)xAttr, (string)yAttr));
                }
                return series;
            });
    }
}

首先,此方法使用 XmlSchema.Read() 方法调用加载 XML 模式。然后,它创建 XmlReader 读取器对象,使用 XDocument doc = XDocument.Load(reader) 加载 XML 文档,并使用 Validate 扩展方法验证加载的 XML。如果此时没有发生错误,则数据将被加载并根据模式进行验证。

LoadWithSchema 方法使用以下方式获取根元素

XNamespace xns = dataNamespaceName;
XElement items = doc.Element(xns + "Items");

注意 xns 变量:它确保 Items 元素定义在正确的 XML 命名空间中。之后,LoadWithSchema 方法解析加载的 XML 并使用以下方式返回结果

return items.Elements().Select<XElement, DataPointSeries>(...)

DataPointSeries 实例由lambda语句创建,该语句

  1. 使用 getXYTypeStrings 方法从 XElement 标签名称中提取数据系列基本类型。
  2. 获取可选属性。
  3. 创建 DataPointSeries 类的实例。从验证后的 IXmlSchemaInfo 实例(与 Point 元素的 x 和 y 属性相关联)中提取 DataPointSeries 类实例的 XXsdTypeStringYXsdTypeString 属性值。XClrTypeYClrType 属性值保留为 null
  4. Points 集合填充该实例的 XsdPoints 属性。

LoadWithSchema 方法的一些重载只有一个参数。这些重载使用存储在 Loader 程序集中的默认模式作为资源。

使用映射加载

主要的 LoadWithMappings 方法重载是

public static IEnumerable<DataPointSeries> 
       LoadWithMappings(TextReader dataReader, TextReader mappingReader)
{
    // Load mappings.
    List<Mapping> mappings = Mapping.Load(mappingReader);

    // Prepaire XmlReaderSettings for input file validation.
    XmlReaderSettings settings = new XmlReaderSettings();
    settings.ValidationType = ValidationType.Schema;
    settings.Schemas.Add(SchemaBuilder.Build(mappings));
    StringBuilder sbErrors = null;
    List<ValidationException.ValidationError> errors = null;
    settings.ValidationEventHandler += (sender, e) =>
    {
        if (sbErrors == null)
            sbErrors = new StringBuilder();
        sbErrors.AppendFormat(
             "Validation error: {1}{0}Line={2}, position={3}{0}",
             System.Environment.NewLine, e.Exception.Message, 
             e.Exception.LineNumber, e.Exception.LinePosition);
        if (errors == null)
            errors = new List<ValidationException.ValidationError>();
        errors.Add(new ValidationException.ValidationError()
        {
            Message = e.Exception.Message,
            Line = e.Exception.LineNumber,
            Position = e.Exception.LinePosition
        });
    };

    // Load and validate the file.
    using (XmlReader reader = XmlReader.Create(dataReader, settings))
    {
        XElement items = XElement.Load(reader);
        if (sbErrors != null)
            // Validation error(s) occured.
            throw new ValidationException(sbErrors.ToString(), errors.ToArray());

        XNamespace xns = dataNamespaceName;
        // Check the root element name (i.e. Items in "urn:PointSeries-schema" xmlns).
        if (items.Name != xns + "Items")
            throw new ValidationException(string.Format("Root element {0} missed", 
                                                        xns + "Items"));
        // Parse the Point.XXX elements.
        return items.Elements().Select<XElement, DataPointSeries>(
            (item) =>
            {
                // Parse item tag name for X/Y type strings.
                string xType, yType;
                getXYTypeStrings(item.Name.ToString(), out xType, out yType);
                
                // Dot-separated type string.
                string xyType = xType == yType ? xType : xType + "." + yType;
                Mapping map = (from mapItem in mappings
                               where mapItem.DotSeparatedTypeString == xyType
                               select mapItem).Single();

                // Optional attributes.
                var yName = item.Attribute("YName");
                var xName = item.Attribute("XName");
                
                DataPointSeries series = new DataPointSeries()
                {
                    XName = xName == null ? "" : xName.Value,
                    XXsdTypeString = map.XAxis.XsdTypeString,
                    XClrType = map.XAxis.ClrType,
                    XTypeString = map.XAxis.TypeString,
                    YName = yName == null ? "" : yName.Value,
                    YXsdTypeString = map.YAxis.XsdTypeString,
                    YClrType = map.YAxis.ClrType,
                    YTypeString = map.YAxis.TypeString
                };
                foreach (var pt in from pt in item.Elements(xns + "Point") select pt)
                {
                    series.XsdPoints.Add(new XsdDataPoint((string)pt.Attribute("x"), 
                                        (string)pt.Attribute("y")));
                }
                return series;
            });
    }
}

LoadWithMappings 方法调用 List<Mapping> mappings = Mapping.Load(mappingReader) 以从提供的映射读取器实例(稍后查看)构建模式。然后,它使用 reader = XmlReader.Create(fileName, settings) 语句创建 XmlReader 实例,并将 XML 加载到内存中,使用 XElement items = XElement.Load(reader)。如果此时没有发生错误,则数据将被加载并根据生成的模式进行验证。

然后,LoadWithSchema 方法解析加载的 XML 并使用以下方式返回结果

return items.Elements().Select<XElement, DataPointSeries>(...)

DataPointSeries 实例由lambda语句创建,该语句

  1. 使用 getXYTypeStrings 方法从 XElement 标签名称中提取数据系列基本类型。
  2. 获取与 XElement 关联的 Mapping 类实例。
  3. 获取可选属性。
  4. 创建 DataPointSeries 类的实例。该实例的 XXsdTypeStringYXsdTypeStringXClrTypeYClrType 属性值从 Mapping 类实例中获取。
  5. Points 集合填充该实例的 XsdPoints 属性。

从映射 XML 构建 XML 模式

XML-CLR-字符串类型映射 XML 文档及其关联的 XML 模式在上面已描述。在代码中,这种映射由两个类表示。

第一个表示一个维度的 XML-CLR-字符串类型映射

/// <summary>
/// XML-CLR-string type mapping in one dimension.
/// </summary>
public class AxisMapping
{
    /// <summary>
    /// Initializes a new instance of the <see cref="AxisMapping"/> class.
    /// </summary>
    /// <param name="xsdType">XML Type Name.</param>
    /// <param name="clrType">CLR Type Name.</param>
    /// <param name="typeString">The type string.</param>
    public AxisMapping(string xsdType, string clrType, string typeString)
    {
        XsdTypeString = xsdType;
        ClrType = string.IsNullOrEmpty(clrType) ? null : Type.GetType(clrType);
        TypeString = typeString;
    }

    /// <summary>
    /// Gets XML atomic type name like "double" or "gMonth".
    /// </summary>
    /// <value>XML atomic type name string.</value>
    /// <remarks>XSD type name string doesn't
    ///   contains namespace prefix. </remarks>
    public string XsdTypeString { get; private set; }

    /// <summary>
    /// Gets the CLR type.
    /// </summary>
    /// <value>The CLR type or <c>null</c>.</value>
    public Type ClrType { get; private set; }

    /// <summary>
    /// Gets the "type string" assigned to this mapping like Double, Int, etc.
    /// </summary>
    /// <value>The type string.</value>
    /// <remarks>"Type string" is used in XML
    ///         schema construction.</remarks>
    public string TypeString { get; private set; }

    ...
}

第二个包含 x、y 维度的 XML-CLR-字符串类型映射,并定义了一些 Load 方法重载以从映射 XML 文档加载映射

/// <summary>
/// X,Y dimensions XML-CLR-string type mappings.
/// </summary>
/// <remarks>
/// <see cref="IEquatable{Mapping}"/> interface
/// implemented for use with the Distinct() LINQ operator.
/// <para>In order to compare the elements,
/// the Distinct operator uses the elements'
/// implementation of the IEquatable<T>.Equals method if the elements
/// implement the IEquatable<T> interface.
/// It uses their implementation of the
/// Object.Equals method otherwise.</para>
/// </remarks>
public class Mapping : IEquatable<Mapping> 
{
    public AxisMapping XAxis { get; private set; }
    public AxisMapping YAxis { get; private set; }

    /// <summary>
    /// Loads XML-CLR-string type mappings from the
    /// <see cref="System.IO.TextReader"/> specified.
    /// </summary>
    /// <param name="mappingReader">Input Mapping XML
    /// <see cref="System.IO.TextReader"/>.</param>
    /// <returns>List of <see cref="Mapping"/> objects.</returns>
    /// <remarks>
    /// <see cref="Mapping"/> instances
    /// with recurring <see cref="TypeString"/> property 
    /// values are removed from output.
    /// </remarks>
    /// <exception cref="ValidationException"/>
    /// <exception cref="RecurringMappingEntriesException"/>
    public static List<Mapping> Load(TextReader mappingReader) { ... }
    /// <summary>
    /// Loads XML-CLR-string type mappings from the
    /// <see cref="System.IO.Stream"/> specified.
    /// </summary>
    /// <param name="stm">Input Mapping XML data
    /// <see cref="System.IO.Stream"/>.</param>
    /// <returns>List of <see cref="Mapping"/> objects.</returns>
    /// <remarks>
    /// <see cref="Mapping"/> instances with
    /// recurring <see cref="TypeString"/> property 
    /// values are removed from output.
    /// </remarks>
    /// <exception cref="ValidationException"/>
    /// <exception cref="RecurringMappingEntriesException"/>
    public static List<Mapping> Load(Stream stm) { ... }
    /// <summary>
    /// Loads XML-CLR-string type mappings from the file specified.
    /// </summary>
    /// <param name="mappingFileName">Mapping file name.</param>
    /// <returns>List of <see cref="Mapping"/> objects.</returns>
    /// <remarks>
    /// <see cref="Mapping"/> instances with recurring
    /// <see cref="TypeString"/> property values are removed from output.
    /// </remarks>
    /// <exception cref="ValidationException"/>
    /// <exception cref="RecurringMappingEntriesException"/>
    public static List<Mapping> Load(string mappingFileName) { ... }

    ...
}

主要的 Load 方法重载如下

public static List<Mapping> Load(TextReader mappingReader)
{
    // XML Mapping Schema resource name.
    const string mappingSchemaResourceName = "typemappings.xsd";
    // XML namespace must be used in XML mappings files.
    const string mappingNamespaceName = "urn:PointSeries-mapping";
    // Mapping element attributes.
    const string attrNameXsdType = "xsd-type"
        , attrNameClrType = "clr-type"
        , attrNameTypeString = "type-string";

    // Get xml schema stream from the "mappingSchemaFileName" resource.
    Assembly assembly = Assembly.GetAssembly(typeof(Loader));
    ResourceManager rm = new ResourceManager(assembly.GetName().Name + 
                                             ".g", assembly);
    using (XmlTextReader schemaReader = 
           new XmlTextReader(rm.GetStream(mappingSchemaResourceName)))
    {
        // Prepaire XmlReaderSettings for input file validation.
        XmlReaderSettings settings = new XmlReaderSettings();
        settings.ValidationType = ValidationType.Schema;
        settings.Schemas.Add(mappingNamespaceName, schemaReader);
        StringBuilder sbErrors = null;
        List<ValidationException.ValidationError> errors = null;
        settings.ValidationEventHandler += (sender, e) =>
        {
            if (sbErrors == null)
                sbErrors = new StringBuilder();
            sbErrors.AppendFormat(
                "Validation error: {1}{0}Line={2}, position={3}{0}", 
                System.Environment.NewLine, e.Exception.Message, 
                e.Exception.LineNumber, e.Exception.LinePosition);
            if (errors == null)
                errors = new List<ValidationException.ValidationError>();
            errors.Add(new ValidationException.ValidationError()
            {
                Message = e.Exception.Message,
                Line = e.Exception.LineNumber,
                Position = e.Exception.LinePosition
            });
        };

        // Load and validate the file.
        using (XmlReader reader = XmlReader.Create(mappingReader, settings))
        {
            XElement mappings = XElement.Load(reader);
            if (sbErrors != null)
                // Validation error(s) occured.
                throw new ValidationException("Mapping file validation errors\n"
                    + sbErrors.ToString(), errors.ToArray());

            XNamespace xns = mappingNamespaceName;
            // Check the root element name
            // (i.e. Mappings in "urn:PointSeries-mapping" xmlns).
            if (mappings.Name != xns + "Mappings")
                throw new ValidationException(string.Format("Root element {0} missed", 
                                                            xns + "Items"));
            // Parse the Mapping elements.
            List<Mapping> mappingList = (from mapping in mappings.Elements(xns + "Mapping")
                    let xAxis = mapping.Element(xns + "XAxis")
                    let yAxis = mapping.Element(xns + "YAxis")
                    select new Mapping()
                    {
                        XAxis = new AxisMapping((string)xAxis.Attribute(attrNameXsdType),
                            (string)xAxis.Attribute(attrNameClrType),
                            (string)xAxis.Attribute(attrNameTypeString)),
                        YAxis = new AxisMapping((string)yAxis.Attribute(attrNameXsdType),
                            (string)yAxis.Attribute(attrNameClrType),
                            (string)yAxis.Attribute(attrNameTypeString))
                    }
                ).ToList();

            // Check result for recurring entries.
            List<Mapping> recurring = new List<Mapping>();
            for (int i = 0; i < mappingList.Count - 1; i++)
            {
                Mapping map = mappingList[i];
                for (int j = i + 1; j < mappingList.Count; j++)
                {
                    Mapping map1 = mappingList[j];
                    if (map.DotSeparatedTypeString == map1.DotSeparatedTypeString)
                        recurring.Add(map1);
                }
            }
            if (recurring.Count > 0)
            {
                StringBuilder sb = 
                  new StringBuilder("Recurring entries found in the mapping file:");
                foreach (Mapping map in recurring)
                {
                    sb.Append(System.Environment.NewLine + map.ToString());
                }
                throw new RecurringMappingEntriesException(sb.ToString(), 
                                                           recurring.ToArray());
            }

            return mappingList;
        }
    }
}

映射 XML 模式文件作为资源存储在 Loader 库程序集中。Load 方法使用 ResourceManager 获取它,并使用它来准备 XmlReaderSettings 类实例,以便加载映射 XML 文档并进行验证。然后,Load 方法使用 XmlReader 加载映射 XML,并通过LINQ查询将其内容转换为 Mapping 对象集合。最后,它检查 Mapping 对象集合是否包含重复条目,如果是,则抛出 RecurringMappingEntriesException

加载数据表示

数据加载的结果存储在 DataPointSeries 对象集合中。

DataPointSeries 类包含描述 x、y 维度类型(基于 XML 和 CLR)的属性。加载的点作为 Collection<XsdDataPoint>DataPointSeries.XsdPoints 属性返回。XsdDataPoint 结构将 x、y 点坐标值存储为字符串,其格式与它们在输入 XML 文件中出现时相同。

要获取类型化的数据点,您应该使用

public IEnumerable<DataPoint> GetPoints(IXmlTypeConverter converter)
该方法使用调用者提供的 XML 到 CLR 类型转换器,将 XsdDataPoint x、y 字段的字符串值转换为特定的 CLR 类型。或者,您可以使用不带参数的 GetPoints 方法重载。它使用硬编码在 Loader 程序集中的默认转换器。

请注意,DataPoint 类将 x、y 值存储在 System.Object 类型的字段中。我们可以转向更类型安全的世界,但使用 C# 3.0,迟早我们会被迫以 System.Object 的形式返回或获取这些值并使用反射来处理它们。让我们等待 C# 4.0 的动态类型。

Using the Code

本文附带的代码包含针对 .NET Framework 3.5 的 Visual Studio 2008 SP1 解决方案,其中包含三个项目。主要部分是上面描述的 Loader 类库项目。

另外两个项目是简单的控制台应用程序,它们从第一个命令行参数指向的 XML 文件加载数据,(对于第二个项目)从第二个参数指向的映射 XML 文件加载数据。它们要么报告错误,要么显示 XML 数据解析的结果。这些应用程序的示例输入文件位于根解决方案目录中。

请注意单元测试项目。它包含了大量数据点系列类型的测试,并为您提供了关于 XML 格式支持的数据以及它们应如何显示的示例。

历史

  • 2009 年 4 月 9 日:初始发布。
  • 2009 年 4 月 16 日:第二次文章修订,增加以下内容
    1. 增加了对即时 XML 模式生成的支持。
    2. 修改了 Loader 类的接口,使其可以通过默认模式、调用者提供的模式或从类型映射 XML 文件生成的模式来加载数据点系列 XML 数据。
    3. 增加了 IXmlConverter 接口及其默认实现。
    4. 修改了 DataPointSeries 类的接口,使其返回数据点系列 XML 数据解析的结果,可以是一个原始 XsdDataPoint 对象集合,也可以是类型安全的 DataPoint 对象。
© . All rights reserved.