.NET - 是否可以在同一对象中同时使用 XmlAnyElementAttribute 和 XmlSerializer.UnknownElement 事件
.NET - Is it possible to use both XmlAnyElementAttribute and XmlSerializer.UnknownElement event within the same object
我有一个 class,其中我不得不将 属性 的类型从简单的 List<string>
更改为复杂的 List<CustomObject>
。
我的问题是,在一段时间内,我会有人使用旧版和新版软件。到目前为止,当我更改合同时,我只是使用 UnknownElement
事件将旧成员映射到新成员,因为它是 private 文件并且它完美地用于向后兼容性但破坏了旧版本,因为它没有写回旧格式。
但这次是共享文件,这让我意识到我错过了向上兼容性,使用旧版本的人会删除新成员。我读到 XmlAnyElementAttribute
以保留未知元素并将它们序列化回文件。这修复了向上兼容性。
我现在已经掌握了拼图的所有部分,但我找不到如何让它们协同工作,因为添加 XmlAnyElementAttribute
似乎以 UnknownElement
未被触发而告终。
我还想简单地回读缺少反序列化事件的 XmlAnyElementAttributeproperty once the deserialization is done but this time, it is the
XmlSerializer。
这是两个文件的示例:
旧格式:
<OptionsSerializable xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<ListeCategories>
<string>SX00</string>
<string>SX01</string>
</ListeCategories>
</OptionsSerializable>
新格式:
<OptionsSerializable xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<ListeCategoriesExt>
<CategoryInfo Name="SX00" Type="Principal" Persistence="Global">
<ToolTip>SX00</ToolTip>
<SearchTerm>SX00</SearchTerm>
</CategoryInfo>
<CategoryInfo Name="SX01" Type="Principal" Persistence="Global">
<ToolTip>SX01</ToolTip>
<SearchTerm>SX01</SearchTerm>
</CategoryInfo>
</ListeCategoriesExt>
</OptionsSerializable>
需要:
<OptionsSerializable xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<ListeCategories>
<string>SX00</string>
<string>SX01</string>
</ListeCategories>
<ListeCategoriesExt>
<CategoryInfo Name="SX00" Type="Principal" Persistence="Global">
<ToolTip>SX00</ToolTip>
<SearchTerm>SX00</SearchTerm>
</CategoryInfo>
<CategoryInfo Name="SX01" Type="Principal" Persistence="Global">
<ToolTip>SX01</ToolTip>
<SearchTerm>SX01</SearchTerm>
</CategoryInfo>
</ListeCategoriesExt>
</OptionsSerializable>
根据 docs:
XmlSerializer.UnknownElement
... Occurs when the XmlSerializer encounters an XML element of unknown type during deserialization.
如果您的 <ListeCategories>
元素绑定到 [XmlAnyElement]
属性,则它们不是未知类型,因此不会引发任何事件。
现在,如果除了 <ListeCategories>
之外还有一些 other 未知元素(未显示在您的问题中),您希望使用 post-process UnknownElement
,您可以通过使用 [XmlAnyElementAttribute(string name)]
:
限制绑定元素的名称来实现
Initializes a new instance of the XmlAnyElementAttribute
class and specifies the XML element name generated in the XML document.
即:
public class OptionsSerializable
{
[XmlAnyElement("ListeCategories")]
public XmlElement [] ListeCategories { get; set; }
现在还有其他未知元素,例如<SomeOtherObsoleteNodeToPostprocess />
,仍会引发该事件。演示 fiddle #1 here。但是您仍然不会收到 <ListeCategories>
.
的事件回调
那么,您有哪些选择?
首先,您可以在 setter 中对 XmlElement []
数组进行 post 处理,如 this answer to Better IXmlSerializable format?:
[XmlRoot(ElementName="OptionsSerializable")]
public class OptionsSerializable
{
[XmlAnyElement("ListeCategories")]
public XmlElement [] ListeCategories
{
get
{
// Convert the ListeCategoriesExt items property to an array of XmlElement
}
set
{
// Convert array of XmlElement back to ListeCategoriesExt items.
}
}
原始的 UnknownElement
事件逻辑也可以通过使用这个来部分保留:
XmlElement[] _unsupported;
[XmlAnyElement()]
public XmlElement[] Unsupported {
get {
return _unsupported;
}
set {
_unsupported = value;
if ((value.Count > 0)) {
foreach (element in value) {
OnUnknownElementFound(this, new XmlElementEventArgs(){Element=element});
}
}
}
}
但是,如果 post 处理是由 OptionsSerializable
对象本身完成的,那么将 ListeCategories
视为 ListeCategoriesExt
属性。以下是我的做法:
[XmlRoot(ElementName="OptionsSerializable")]
public class OptionsSerializable
{
[XmlArray("ListeCategories"), XmlArrayItem("string")]
public string [] XmlListeCategories
{
//Can't use [Obsolete] because doing so will cause XmlSerializer to not serialize the property, see
get
{
// Since it seems <CategoryInfo Name="VerifierCoherence" Type="Principal" Persistence="Global"> should not be written back,
// you will need to add a .Where clause excluding those CategoryInfo items you don't want to appear in the old list of strings.
return ListeCategoriesExt?.Select(c => c.Name)?.ToArray();
}
set
{
// Merge in the deserialization results. Note this algorithm assumes that there are no duplicate names.
// Convert array of XmlElement back to ListeCategoriesExt items.
foreach (var name in value)
{
if (ListeCategoriesExt.FindIndex(c => c.Name == name) < 0)
{
ListeCategoriesExt.Add(new CategoryInfo
{
Name = name, Type = "Principal", Persistence = "Global",
ToolTip = name,
SearchTerm = name,
});
}
}
}
}
[XmlArray("ListeCategoriesExt"), XmlArrayItem("CategoryInfo")]
public CategoryInfo [] XmlListeCategoriesExt
{
get
{
return ListeCategoriesExt?.ToArray();
}
set
{
// Merge in the deserialization results. Note this algorithm assumes that there are no duplicate names.
foreach (var category in value)
{
var index = ListeCategoriesExt.FindIndex(c => c.Name == category.Name);
if (index < 0)
{
ListeCategoriesExt.Add(category);
}
else
{
// Overwrite the item added during XmlListeCategories deserialization.
ListeCategoriesExt[index] = category;
}
}
}
}
[XmlIgnore]
public List<CategoryInfo> ListeCategoriesExt { get; set; } = new List<CategoryInfo>();
}
[XmlRoot(ElementName="CategoryInfo")]
public class CategoryInfo
{
[XmlElement(ElementName="ToolTip")]
public string ToolTip { get; set; }
[XmlElement(ElementName="SearchTerm")]
public string SearchTerm { get; set; }
[XmlAttribute(AttributeName="Name")]
public string Name { get; set; }
[XmlAttribute(AttributeName="Type")]
public string Type { get; set; }
[XmlAttribute(AttributeName="Persistence")]
public string Persistence { get; set; }
}
备注:
由于<ListeCategories>
出现在之前 <ListeCategoriesExt>
在您的XML中,需要将新项目合并到XmlListeCategoriesExt
setter 中先前反序列化的过时项目。
如果您要设置 XmlArrayAttribute.Order
两者都需要 <ListeCategories>
来 last.
[=90,则没有必要这样做=]
由于合并的需要,反序列化算法不支持多个CategoryInfo
同名对象。
如果您的 CategoryInfo
列表中必须有相同的名称,则合并新旧表示会变得更加复杂。
不幸的是,无法在 OnDeserialized
事件中合并新旧类别列表,因为令人讨厌的是,XmlSerializer
does not support [OnDeserialized]
.
演示 fiddle #2 here.
我有一个 class,其中我不得不将 属性 的类型从简单的 List<string>
更改为复杂的 List<CustomObject>
。
我的问题是,在一段时间内,我会有人使用旧版和新版软件。到目前为止,当我更改合同时,我只是使用 UnknownElement
事件将旧成员映射到新成员,因为它是 private 文件并且它完美地用于向后兼容性但破坏了旧版本,因为它没有写回旧格式。
但这次是共享文件,这让我意识到我错过了向上兼容性,使用旧版本的人会删除新成员。我读到 XmlAnyElementAttribute
以保留未知元素并将它们序列化回文件。这修复了向上兼容性。
我现在已经掌握了拼图的所有部分,但我找不到如何让它们协同工作,因为添加 XmlAnyElementAttribute
似乎以 UnknownElement
未被触发而告终。
我还想简单地回读缺少反序列化事件的 XmlAnyElementAttributeproperty once the deserialization is done but this time, it is the
XmlSerializer。
这是两个文件的示例: 旧格式:
<OptionsSerializable xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<ListeCategories>
<string>SX00</string>
<string>SX01</string>
</ListeCategories>
</OptionsSerializable>
新格式:
<OptionsSerializable xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<ListeCategoriesExt>
<CategoryInfo Name="SX00" Type="Principal" Persistence="Global">
<ToolTip>SX00</ToolTip>
<SearchTerm>SX00</SearchTerm>
</CategoryInfo>
<CategoryInfo Name="SX01" Type="Principal" Persistence="Global">
<ToolTip>SX01</ToolTip>
<SearchTerm>SX01</SearchTerm>
</CategoryInfo>
</ListeCategoriesExt>
</OptionsSerializable>
需要:
<OptionsSerializable xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<ListeCategories>
<string>SX00</string>
<string>SX01</string>
</ListeCategories>
<ListeCategoriesExt>
<CategoryInfo Name="SX00" Type="Principal" Persistence="Global">
<ToolTip>SX00</ToolTip>
<SearchTerm>SX00</SearchTerm>
</CategoryInfo>
<CategoryInfo Name="SX01" Type="Principal" Persistence="Global">
<ToolTip>SX01</ToolTip>
<SearchTerm>SX01</SearchTerm>
</CategoryInfo>
</ListeCategoriesExt>
</OptionsSerializable>
根据 docs:
XmlSerializer.UnknownElement
... Occurs when the XmlSerializer encounters an XML element of unknown type during deserialization.
如果您的 <ListeCategories>
元素绑定到 [XmlAnyElement]
属性,则它们不是未知类型,因此不会引发任何事件。
现在,如果除了 <ListeCategories>
之外还有一些 other 未知元素(未显示在您的问题中),您希望使用 post-process UnknownElement
,您可以通过使用 [XmlAnyElementAttribute(string name)]
:
Initializes a new instance of the
XmlAnyElementAttribute
class and specifies the XML element name generated in the XML document.
即:
public class OptionsSerializable
{
[XmlAnyElement("ListeCategories")]
public XmlElement [] ListeCategories { get; set; }
现在还有其他未知元素,例如<SomeOtherObsoleteNodeToPostprocess />
,仍会引发该事件。演示 fiddle #1 here。但是您仍然不会收到 <ListeCategories>
.
那么,您有哪些选择?
首先,您可以在 setter 中对 XmlElement []
数组进行 post 处理,如 this answer to Better IXmlSerializable format?:
[XmlRoot(ElementName="OptionsSerializable")]
public class OptionsSerializable
{
[XmlAnyElement("ListeCategories")]
public XmlElement [] ListeCategories
{
get
{
// Convert the ListeCategoriesExt items property to an array of XmlElement
}
set
{
// Convert array of XmlElement back to ListeCategoriesExt items.
}
}
原始的 UnknownElement
事件逻辑也可以通过使用这个来部分保留:
XmlElement[] _unsupported;
[XmlAnyElement()]
public XmlElement[] Unsupported {
get {
return _unsupported;
}
set {
_unsupported = value;
if ((value.Count > 0)) {
foreach (element in value) {
OnUnknownElementFound(this, new XmlElementEventArgs(){Element=element});
}
}
}
}
但是,如果 post 处理是由 OptionsSerializable
对象本身完成的,那么将 ListeCategories
视为 ListeCategoriesExt
属性。以下是我的做法:
[XmlRoot(ElementName="OptionsSerializable")]
public class OptionsSerializable
{
[XmlArray("ListeCategories"), XmlArrayItem("string")]
public string [] XmlListeCategories
{
//Can't use [Obsolete] because doing so will cause XmlSerializer to not serialize the property, see
get
{
// Since it seems <CategoryInfo Name="VerifierCoherence" Type="Principal" Persistence="Global"> should not be written back,
// you will need to add a .Where clause excluding those CategoryInfo items you don't want to appear in the old list of strings.
return ListeCategoriesExt?.Select(c => c.Name)?.ToArray();
}
set
{
// Merge in the deserialization results. Note this algorithm assumes that there are no duplicate names.
// Convert array of XmlElement back to ListeCategoriesExt items.
foreach (var name in value)
{
if (ListeCategoriesExt.FindIndex(c => c.Name == name) < 0)
{
ListeCategoriesExt.Add(new CategoryInfo
{
Name = name, Type = "Principal", Persistence = "Global",
ToolTip = name,
SearchTerm = name,
});
}
}
}
}
[XmlArray("ListeCategoriesExt"), XmlArrayItem("CategoryInfo")]
public CategoryInfo [] XmlListeCategoriesExt
{
get
{
return ListeCategoriesExt?.ToArray();
}
set
{
// Merge in the deserialization results. Note this algorithm assumes that there are no duplicate names.
foreach (var category in value)
{
var index = ListeCategoriesExt.FindIndex(c => c.Name == category.Name);
if (index < 0)
{
ListeCategoriesExt.Add(category);
}
else
{
// Overwrite the item added during XmlListeCategories deserialization.
ListeCategoriesExt[index] = category;
}
}
}
}
[XmlIgnore]
public List<CategoryInfo> ListeCategoriesExt { get; set; } = new List<CategoryInfo>();
}
[XmlRoot(ElementName="CategoryInfo")]
public class CategoryInfo
{
[XmlElement(ElementName="ToolTip")]
public string ToolTip { get; set; }
[XmlElement(ElementName="SearchTerm")]
public string SearchTerm { get; set; }
[XmlAttribute(AttributeName="Name")]
public string Name { get; set; }
[XmlAttribute(AttributeName="Type")]
public string Type { get; set; }
[XmlAttribute(AttributeName="Persistence")]
public string Persistence { get; set; }
}
备注:
由于
<ListeCategories>
出现在之前<ListeCategoriesExt>
在您的XML中,需要将新项目合并到XmlListeCategoriesExt
setter 中先前反序列化的过时项目。如果您要设置
[=90,则没有必要这样做=]XmlArrayAttribute.Order
两者都需要<ListeCategories>
来 last.由于合并的需要,反序列化算法不支持多个
CategoryInfo
同名对象。如果您的
CategoryInfo
列表中必须有相同的名称,则合并新旧表示会变得更加复杂。不幸的是,无法在
OnDeserialized
事件中合并新旧类别列表,因为令人讨厌的是,XmlSerializer
does not support[OnDeserialized]
.
演示 fiddle #2 here.