在.NET中正确地序列化和反序列化"混合"类型的XML

19

我目前的任务是编写一个用于处理HL7 CDA文件的类库。这些HL7 CDA文件是带有定义的XML架构的XML文件,因此我使用了xsd.exe来为XML序列化和反序列化生成.NET类。

XML Schema包含各种类型,其中包含mixed="true"属性,指定此类型的XML节点可以包含普通文本与其他XML节点混合。
对于其中一种类型,XML模式的相关部分如下:

<xs:complexType name="StrucDoc.Paragraph" mixed="true">
    <xs:sequence>
        <xs:element name="caption" type="StrucDoc.Caption" minOccurs="0"/>
        <xs:choice minOccurs="0" maxOccurs="unbounded">
            <xs:element name="br" type="StrucDoc.Br"/>
            <xs:element name="sub" type="StrucDoc.Sub"/>
            <xs:element name="sup" type="StrucDoc.Sup"/>
            <!-- ...other possible nodes... -->
        </xs:choice>
    </xs:sequence>
    <xs:attribute name="ID" type="xs:ID"/>
    <!-- ...other attributes... -->
</xs:complexType>

此类型的生成代码如下:

/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "2.0.50727.3038")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(TypeName="StrucDoc.Paragraph", Namespace="urn:hl7-org:v3")]
public partial class StrucDocParagraph {

    private StrucDocCaption captionField;

    private object[] itemsField;

    private string[] textField;

    private string idField;

    // ...fields for other attributes...

    /// <remarks/>
    public StrucDocCaption caption {
        get {
            return this.captionField;
        }
        set {
            this.captionField = value;
        }
    }

    /// <remarks/>
    [System.Xml.Serialization.XmlElementAttribute("br", typeof(StrucDocBr))]
    [System.Xml.Serialization.XmlElementAttribute("sub", typeof(StrucDocSub))]
    [System.Xml.Serialization.XmlElementAttribute("sup", typeof(StrucDocSup))]
    // ...other possible nodes...
    public object[] Items {
        get {
            return this.itemsField;
        }
        set {
            this.itemsField = value;
        }
    }

    /// <remarks/>
    [System.Xml.Serialization.XmlTextAttribute()]
    public string[] Text {
        get {
            return this.textField;
        }
        set {
            this.textField = value;
        }
    }

    /// <remarks/>
    [System.Xml.Serialization.XmlAttributeAttribute(DataType="ID")]
    public string ID {
        get {
            return this.idField;
        }
        set {
            this.idField = value;
        }
    }

    // ...properties for other attributes...
}

如果我对一个XML元素进行反序列化,其中段落节点看起来像这样:

<paragraph>first line<br /><br />third line</paragraph>

结果是,项目和文本数组的读取方式如下:

itemsField = new object[]
{
    new StrucDocBr(),
    new StrucDocBr(),
};
textField = new string[]
{
    "first line",
    "third line",
};

从这里无法确定文本和其他节点的确切顺序。
如果我再次进行序列化,结果看起来与此完全相同:

<paragraph>
    <br />
    <br />first linethird line
</paragraph>
默认的序列化器只是先序列化项目,然后再序列化文本。
我尝试在 StrucDocParagraph 类上实现 IXmlSerializable,以便我可以控制内容的反序列化和序列化,但由于涉及到许多类,所以它相当复杂,并且我还没有找到解决方案,因为我不知道这样做是否值得。
有没有一种容易的解决方法来解决这个问题,或者通过 IXmlSerializable 进行自定义序列化甚至可能吗? 还是我应该使用 XmlDocument 或 XmlReader/XmlWriter 来处理这些文档?
3个回答

22
为了解决这个问题,我不得不修改生成的类:
  1. XmlTextAttributeText 属性移动到 Items 属性,并添加参数 Type = typeof(string)
  2. 删除 Text 属性
  3. 删除 textField 字段
因此,生成的代码(修改后)如下所示:
/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "2.0.50727.3038")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(TypeName="StrucDoc.Paragraph", Namespace="urn:hl7-org:v3")]
public partial class StrucDocParagraph {

    private StrucDocCaption captionField;

    private object[] itemsField;

    private string idField;

    // ...fields for other attributes...

    /// <remarks/>
    public StrucDocCaption caption {
        get {
            return this.captionField;
        }
        set {
            this.captionField = value;
        }
    }

    /// <remarks/>
    [System.Xml.Serialization.XmlElementAttribute("br", typeof(StrucDocBr))]
    [System.Xml.Serialization.XmlElementAttribute("sub", typeof(StrucDocSub))]
    [System.Xml.Serialization.XmlElementAttribute("sup", typeof(StrucDocSup))]
    // ...other possible nodes...
    [System.Xml.Serialization.XmlTextAttribute(typeof(string))]
    public object[] Items {
        get {
            return this.itemsField;
        }
        set {
            this.itemsField = value;
        }
    }

    /// <remarks/>
    [System.Xml.Serialization.XmlAttributeAttribute(DataType="ID")]
    public string ID {
        get {
            return this.idField;
        }
        set {
            this.idField = value;
        }
    }

    // ...properties for other attributes...
}

现在,如果我反序列化一个XML元素,其中段落节点看起来像这样:

<paragraph>first line<br /><br />third line</paragraph>

结果是这样读取项数组的:

itemsField = new object[]
{
    "first line",
    new StrucDocBr(),
    new StrucDocBr(),
    "third line",
};

这恰好是我需要的, 项目的顺序和内容都是正确的。
如果我再次进行序列化,结果仍然是正确的:

<paragraph>first line<br /><br />third line</paragraph>

启发我正确方向的是Guillaume的答案,我也认为应该可以这样做。然后在MSDN文档中找到了XmlTextAttribute的解释:

你可以将XmlTextAttribute应用于返回字符串数组的字段或属性。你还可以将该属性应用于Object类型的数组,但必须设置Type属性为string。在这种情况下,插入到数组中的任何字符串都会被序列化为XML文本。

因此,现在序列化和反序列化都正常工作,但我不知道是否有其他副作用。也许不再能使用xsd.exe从这些类生成模式,但我也不需要那个。


这个似乎不再起作用了(我的 System.Xml 版本是 4.0.0)。问题在于,它通过一个 ItemsElementName 字符串数组跟踪 Items 数组中元素的名称,而这些元素必须一一对应。这个要求会在您从反序列化 XML 文档填充的对象模型中操作时导致错误,因为 XMLSerializer 不会为它们在 ItemsElementName 数组中放置代表性条目。因此,文本节点后面是一个 xml 元素,然后是一个文本节点,Items 数组中会有 3 个条目,但在 ItemsElementName 中只有 1 个。 - theta-fish
谢谢,我也遇到了HL7 CDA模式的完全相同的问题,这个方法完美解决了 :) - user544511

3

我遇到了和这个问题相同的情况,并且找到了通过修改 xsd.exe 生成的 .cs 文件来解决这个问题的方法。虽然它确实可行,但我不太舒服改变生成的代码,因为每次重新生成类都需要记住这样做。这也导致了一些笨拙的代码,必须测试并转换成 XmlNode[] 来处理 mailto 元素。

我的解决方案是重新考虑 xsd。我放弃使用混合类型,并且基本上定义了自己的混合类型。

我有这个:

XML: <text>some text <mailto>me@email.com</mailto>some more text</text>

<xs:complexType name="text" mixed="true">
    <xs:sequence>
      <xs:element minOccurs="0" maxOccurs="unbounded" name="mailto" type="xs:string" />
    </xs:sequence>
  </xs:complexType>

并更改为

XML: <mytext><text>some text </text><mailto>me@email.com</mailto><text>some more text</text></mytext>

<xs:complexType name="mytext">
    <xs:sequence>
      <xs:choice minOccurs="0" maxOccurs="unbounded">
        <xs:element name="text">
          <xs:complexType>
            <xs:simpleContent>
              <xs:extension base="xs:string" />
            </xs:simpleContent>
          </xs:complexType>
        </xs:element>
        <xs:element name="mailto">
          <xs:complexType>
            <xs:simpleContent>
              <xs:extension base="xs:string" />
            </xs:simpleContent>
          </xs:complexType>
        </xs:element>
      </xs:choice>
    </xs:sequence>
  </xs:complexType>

我的生成代码现在为我提供了一个名为myText的类:
public partial class myText{

    private object[] itemsField;

    /// <remarks/>
    [System.Xml.Serialization.XmlElementAttribute("mailto", typeof(myTextTextMailto))]
    [System.Xml.Serialization.XmlElementAttribute("text", typeof(myTextText))]
    public object[] Items {
        get {
            return this.itemsField;
        }
        set {
            this.itemsField = value;
        }
    }
}

现在元素的顺序在序列化/反序列化中得到了保留,但我需要测试/强制转换/根据类型编写代码myTextTextMailtomyTextText

只是想提供一种替代方法,这对我很有效。


1
我认同你的方法是对于那些定义和使用自己的XML模式的人来说,这个问题的首选解决方案。我的问题在于,我没有修改XSD的选项,因为它由第三方控制。因此,我不得不修改生成的类,正如你所说,只有在没有其他选择的情况下才应该这样做。 - Stefan Podskubka

0

关于什么?

itemsField = new object[] 
{ 
    "first line", 
    new StrucDocBr(), 
    new StrucDocBr(), 
    "third line", 
};

?


1
当我尝试序列化对象时,由于itemsField中包含字符串,因此会导致InvalidOperationException异常(itemsField数组只能包含由公共属性“Items”的[XmlElement]属性指定的那些类型的对象)。 - Stefan Podskubka
你可以在这里寻求帮助: http://msdn.microsoft.com/en-us/library/kz8z99ds.aspx 有模式验证警告吗? - Guillaume
1
我在搜索中已经找到了那个页面,但它是关于另一个问题的。我的XML文档架构是正确的,我在反序列化之前和序列化之后都进行了验证。但是我刚刚找到了解决我的问题的答案,你提供的itemsField数组建议已经接近了,只需要在生成的代码中进行一些进一步的修改。我将在几分钟内发布它。 - Stefan Podskubka

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接