问题描述
我想将一个 XML 文件反序列化为一个具有多个子类的类.XML 如下所示:
I would like to deserialize an XML File to a class with several subclasses. The XML looks like this:
<?xml version="1.0" encoding="utf-8"?>
<Objects>
<Group index="1">
<de>
<GroupName>ANTRIEB</GroupName>
</de>
<en>
<GroupName>missing translation!</GroupName>
</en>
<Level>2</Level>
</Group>
<Group index="2">
<de>
<GroupName>BREMSEN</GroupName>
</de>
<Level>3</Level>
</Group>
</Objects>
如果没有那些语言标签,将 XML 反序列化为类是没有问题的.当然,我可以为每个可能的语言标签创建一个属性.但是可能的语言列表应该是动态的(例如从配置文件中读取).
Deserializing the XML to classes would be no problem, if there wouldn't be those language tags. Sure, I could create a property for every language tag possible. But the list of languages possible should be dynamic (e.g. read from an config file).
这就是为什么我想将这些语言标签及其内容反序列化为一个以该语言为关键字和内容模型的字典的原因.
This is the reason why i would like to deserialize those language tags and their content into a Dictionary which uses the language as key and a model for the content.
我的模型如下所示:
[XmlRoot("Objects")]
public class DeactivationsXml
{
[XmlElement("Group")]
public DeactivationsGroup[] Groups { get; set; }
}
[Serializable()]
public class DeactivationsGroup
{
[XmlIgnore]
public Dictionary<string, GroupName> GroupNames { get; set; } = new Dictionary<string, GroupName>();
public int Level { get; set; }
[XmlAttribute]
public byte index { get; set; }
}
public class GroupName
{
[XmlElement("GroupName")]
public string Name { get; set; }
}
我搜索了很长时间来解决这个问题,但找不到解决方案.我很确定,仅用属性是不可能解决这个问题的.
I searched for a long time to address this problem, but couldn't find a solution. I'm pretty sure, that it's not possible to solve this Problem just with attributes.
是否存在某种混合方法,以便将 XML 文件的反序列化与无法自动反序列化的所有 XmlElement 的手动反序列化相结合?
Does some hybrid aproach exist in order to combine the Deserialization of an XML File in combination with manual deserialization of all XmlElements which could not be automatically deserialized?
对于我的问题,一个好的且可扩展的解决方案会很棒,因为 XML 结构很复杂(同一问题多次出现不同的内容等).我无法更改 XML 的结构,所以请不要指出这一点.
A good and extensible solution for my problem would be great, because the XML structure is complex (same Problem several times with different content etc.).I can't change the structure of the XML, so please don't point this out.
IXmlSerializable
我尝试在 DeactivationsGroup 类上实现 IXmlSerializable 接口,以便使用给定语言的列表搜索具有这些名称的 XmlElements 并反序列化这些 XmlElements 的内容.
I tried to implement the IXmlSerializable Interface on the DeactivationsGroup class in order to search with a list of given languages for XmlElements with those names and deserialize the content of those XmlElements.
但是这种方法行不通,因为您必须手动映射所有属性.
But this approach didn't work out, because you have to map all properties manually.
IExtensibleDataObject
该接口仅由 DataContractSerializer 支持.在最坏的情况下,如果没有找到其他解决方案,我可以在反序列化后使用此接口进行反序列化..
The Interface is only supported by a DataContractSerializer. In the worst case i could use this interface to deserialize after Deserializing, if no other solution is found..
OnDeserialization
XmlSerializer 不支持此属性,但可以提供我可能需要的功能.
This Attribute is not supported by XmlSerializer, but would provide the functionality i possibly need.
XmlAnyElement
我想这是目前最好的选择.反序列化完成后是否存在一些回调以实现自动化?
I guess this is the best option at this point. Does some callback exist after deserialization finished in order to automate this?
这是目前为止的全部代码.
Here's the whole code so far.
public void Parse()
{
string xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
" <Objects>" +
" <Group index=\"1\">" +
" <de>" +
" <GroupName>ANTRIEB</GroupName>" +
" </de>" +
" <en>" +
" <GroupName>missing translation!</GroupName>" +
" </en>" +
" <Level>2</Level>" +
" </Group>" +
" <Group index=\"2\">" +
" <de>" +
" <GroupName>BREMSEN</GroupName>" +
" </de>" +
" <Level>3</Level>" +
" </Group>" +
" </Objects>";
XmlSerializer serializer = new XmlSerializer(typeof(DeactivationsXml));
using (TextReader fileStream = new StringReader(xml))
{
var result = (DeactivationsXml)serializer.Deserialize(fileStream);
}
}
[XmlRoot("Objects")]
public class DeactivationsXml
{
[XmlElement("Group")]
public DeactivationsGroup[] Groups { get; set; }
}
[Serializable()]
public class DeactivationsGroup
{
[XmlIgnore]
public Dictionary<string, GroupName> GroupNames { get; set; } = new Dictionary<string, GroupName>();
public int Level { get; set; }
[XmlAttribute]
public byte index { get; set; }
}
public class GroupName
{
[XmlElement("GroupName")]
public string Name { get; set; }
}
推荐答案
您可以采用 this answer 中的方法和添加代理 XmlElement []
属性,标记为 [XmlAnyElement]
,对 Dictionary
属性的键/值对执行嵌套(反)序列化,将字典键绑定到元素名称.
You can adopt the approach from this answer and add a surrogate XmlElement []
property, marked with [XmlAnyElement]
, that performs a nested (de)serialization on the key/value pairs of the Dictionary<string, GroupName>
property, binding the dictionary keys to the element names.
请注意,虽然文档 用于 XmlAnyElementAttribute
状态
Note that, while the documentation for XmlAnyElementAttribute
states
指定成员(一个返回 XmlElement 或 XmlNode 对象数组的字段)包含的对象代表任何在被序列化或反序列化的对象中没有相应成员的 XML 元素.
事实上,该属性也可以应用于属性.因此不需要(反)序列化回调,因为嵌套序列化可以在代理属性本身的 getter 和 setter 中执行.如果您更喜欢新的 LINQ-to-XML API,它还可以应用于返回 XElement
对象数组而不是 XmlElement
的成员.
In fact the attribute can be applied to a property as well. Thus a (de)serialization callback is not required since the nested serialization can be performed inside the getter and setter for the surrogate property itself. It can also be applied to members returning an array of XElement
objects instead of XmlElement
if you prefer the new LINQ-to-XML API.
在这种方法中,您的 DeactivationsGroup
看起来像:
In this approach, your DeactivationsGroup
would look like:
[Serializable()]
public class DeactivationsGroup
{
public DeactivationsGroup() { this.GroupNames = new Dictionary<string, GroupName>(); }
[XmlIgnore]
public Dictionary<string, GroupName> GroupNames { get; set; }
public int Level { get; set; }
[XmlAttribute]
public byte index { get; set; }
[XmlAnyElement]
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never), DebuggerBrowsable(DebuggerBrowsableState.Never)]
public XElement[] XmlGroupNames
{
get
{
return GroupNames.SerializeToXElements(null);
}
set
{
if (value == null || value.Length < 1)
return;
foreach (var pair in value.DeserializeFromXElements<GroupName>())
{
GroupNames.Add(pair.Key, pair.Value);
}
}
}
}
使用以下扩展方法和类:
Making use of the following extension methods and classes:
public static class XmlKeyValueListHelper
{
const string RootLocalName = "Root";
public static XElement [] SerializeToXElements<T>(this IEnumerable<KeyValuePair<string, T>> dictionary, XNamespace ns)
{
if (dictionary == null)
return null;
ns = ns ?? "";
var serializer = XmlSerializerFactory.Create(typeof(T), RootLocalName, ns.NamespaceName);
var array = dictionary
.Select(p => new { p.Key, Value = p.Value.SerializeToXElement(serializer, true) })
// Fix name and remove redundant xmlns= attributes. XmlWriter will add them back if needed.
.Select(p => new XElement(ns + p.Key, p.Value.Attributes().Where(a => !a.IsNamespaceDeclaration), p.Value.Elements()))
.ToArray();
return array;
}
public static IEnumerable<KeyValuePair<string, T>> DeserializeFromXElements<T>(this IEnumerable<XElement> elements)
{
if (elements == null)
yield break;
XmlSerializer serializer = null;
XNamespace ns = null;
foreach (var element in elements)
{
if (serializer == null || element.Name.Namespace != ns)
{
ns = element.Name.Namespace;
serializer = XmlSerializerFactory.Create(typeof(T), RootLocalName, ns.NamespaceName);
}
var elementToDeserialize = new XElement(ns + RootLocalName, element.Attributes(), element.Elements());
yield return new KeyValuePair<string, T>(element.Name.LocalName, elementToDeserialize.Deserialize<T>(serializer));
}
}
public static XmlSerializerNamespaces NoStandardXmlNamespaces()
{
var ns = new XmlSerializerNamespaces();
ns.Add("", ""); // Disable the xmlns:xsi and xmlns:xsd lines.
return ns;
}
public static XElement SerializeToXElement<T>(this T obj)
{
return obj.SerializeToXElement(null, NoStandardXmlNamespaces());
}
public static XElement SerializeToXElement<T>(this T obj, XmlSerializerNamespaces ns)
{
return obj.SerializeToXElement(null, ns);
}
public static XElement SerializeToXElement<T>(this T obj, XmlSerializer serializer, bool omitStandardNamespaces)
{
return obj.SerializeToXElement(serializer, (omitStandardNamespaces ? NoStandardXmlNamespaces() : null));
}
public static XElement SerializeToXElement<T>(this T obj, XmlSerializer serializer, XmlSerializerNamespaces ns)
{
var doc = new XDocument();
using (var writer = doc.CreateWriter())
(serializer ?? new XmlSerializer(obj.GetType())).Serialize(writer, obj, ns);
var element = doc.Root;
if (element != null)
element.Remove();
return element;
}
public static T Deserialize<T>(this XContainer element, XmlSerializer serializer)
{
using (var reader = element.CreateReader())
{
object result = (serializer ?? new XmlSerializer(typeof(T))).Deserialize(reader);
return (T)result;
}
}
}
public static class XmlSerializerFactory
{
// To avoid a memory leak the serializer must be cached.
// https://stackoverflow.com/questions/23897145/memory-leak-using-streamreader-and-xmlserializer
// This factory taken from
// https://stackoverflow.com/questions/34128757/wrap-properties-with-cdata-section-xml-serialization-c-sharp/34138648#34138648
readonly static Dictionary<Tuple<Type, string, string>, XmlSerializer> cache;
readonly static object padlock;
static XmlSerializerFactory()
{
padlock = new object();
cache = new Dictionary<Tuple<Type, string, string>, XmlSerializer>();
}
public static XmlSerializer Create(Type serializedType, string rootName, string rootNamespace)
{
if (serializedType == null)
throw new ArgumentNullException();
if (rootName == null && rootNamespace == null)
return new XmlSerializer(serializedType);
lock (padlock)
{
XmlSerializer serializer;
var key = Tuple.Create(serializedType, rootName, rootNamespace);
if (!cache.TryGetValue(key, out serializer))
cache[key] = serializer = new XmlSerializer(serializedType, new XmlRootAttribute { ElementName = rootName, Namespace = rootNamespace });
return serializer;
}
}
}
示例小提琴.以及另一个展示了一个带有 XML 命名空间和属性的案例.
Sample fiddle. And another demonstrating a case with XML namespaces and attributes.
这篇关于使用 XmlSerializer 反序列化 XML,其中 XmlElement 名称不同但内容相同的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!