【问题标题】:Deserialize XML with XmlSerializer where XmlElement names differ but have same content使用 XmlSerializer 反序列化 XML,其中 XmlElement 名称不同但内容相同
【发布时间】:2018-01-27 17:27:33
【问题描述】:

我想将一个 XML 文件反序列化为一个具有多个子类的类。 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>

如果没有那些语言标签,将 XML 反序列化为类是没有问题的。当然,我可以为每个可能的语言标签创建一个属性。但是可能的语言列表应该是动态的(例如从配置文件中读取)。

这就是为什么我想将这些语言标签及其内容反序列化为字典,该字典使用语言作为键和内容的模型。

我的模型如下所示:

[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; }
}

我搜索了很长时间来解决这个问题,但找不到解决方案。我很确定,仅仅使用属性是不可能解决这个问题的。

是否存在一些混合方法,以便将 XML 文件的反序列​​化与无法自动反序列化的所有 XmlElement 的手动反序列化相结合?

对于我的问题,一个好的和可扩展的解决方案会很棒,因为 XML 结构很复杂(相同的问题多次出现不同的内容等)。 我无法更改 XML 的结构,所以请不要指出这一点。

方法

IXmlSerializable

我尝试在 DeactivationsGroup 类上实现 IXmlSerializable 接口,以便使用给定语言列表搜索具有这些名称的 XmlElements 并反序列化这些 XmlElements 的内容。

但这种方法行不通,因为您必须手动映射所有属性。

IExtensibleDataObject

该接口仅受 DataContractSerializer 支持。在最坏的情况下,如果没有找到其他解决方案,我可以在反序列化后使用此接口进行反序列化..

OnDeserialization

XmlSerializer 不支持此属性,但会提供我可能需要的功能。

XmlAnyElement

我想这是目前最好的选择。反序列化完成后是否存在一些回调以自动执行此操作?

可执行代码

这是到目前为止的全部代码。

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; }
}

【问题讨论】:

标签: c# xml serialization xmlserializer


【解决方案1】:

您可以采用this answer 中的方法并添加一个代理XmlElement [] 属性,标记为[XmlAnyElement],它对Dictionary&lt;string, GroupName&gt; 属性的键/值对执行嵌套(反)序列化,绑定元素名称的字典键。

请注意,documentation 表示 XmlAnyElementAttribute 状态

指定成员(返回 XmlElement 或 XmlNode 对象数组的字段)包含表示任何 XML 元素的对象,该元素在被序列化或反序列化的对象中没有对应的成员。

事实上,属性也可以应用于属性。因此,不需要(反)序列化回调,因为可以在代理属性本身的 getter 和 setter 内执行嵌套序列化。如果您更喜欢新的 LINQ-to-XML API,它还可以应用于返回 XElement 对象数组而不是 XmlElement 的成员。

在这种方法中,您的 DeactivationsGroup 看起来像:

[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);
            }
        }
    }
}

使用以下扩展方法和类:

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;
        }
    }
}

示例fiddleanother 演示了一个带有 XML 命名空间和属性的案例。

【讨论】:

  • 谢谢你,非常感谢你的努力和这个解决方案。这正是我希望它工作的方式。如果可以的话,我会给你买啤酒。
【解决方案2】:

尝试使用 xml linq 跟踪

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;

namespace ConsoleApplication1
{
    class Program
    {
        const string FILENAME = @"c:\temp\test.xml";
        static void Main(string[] args)
        {
            XDocument doc = XDocument.Load(FILENAME);
            DeactivationsGroup.GroupNames = doc.Descendants("Group").Select(x => new {
                languages = x.Elements().Where(y => y.Element("GroupName") != null).Select(y => new DeactivationsGroup() { 
                   name = (string)y.Element("GroupName"),
                   level = (int)x.Element("Level"),
                   index = byte.Parse((string)x.Attribute("index")),
                   language = y.Name.LocalName
                })
            }).SelectMany(y => y.languages)
            .GroupBy(x => x.name, y => y)
            .ToDictionary(x => x.Key, y => y.FirstOrDefault());

        }
        public class DeactivationsGroup
        {
            public static Dictionary<string, DeactivationsGroup> GroupNames { get; set; }

            public string name { get; set; }
            public int level { get; set; }

            public byte index { get; set; }

            public string language { get; set; }
        }

    }
}

【讨论】:

  • 您好,jdweng,感谢您的帮助。我目前的解决方案就是这样,但不是我想要的。我的原始xml文件的结构非常复杂,导致使用这个方法时代码很多。
  • 使用 LINQ 查询语法(即from where select)将使您的代码更清晰,更易读,imo。
  • 你仍然可以使用你的序列化。只需将自定义私有属性添加到为组执行获取/设置的类。
【解决方案3】:

这很容易做到,如下所示。

使用你的类集。

在序列化器上设置事件处理程序:

var serializer = new XmlSerializer(typeof(DeactivationsXml));
serializer.UnknownElement += Serializer_UnknownElement;

这个处理程序中的代码很简单:

private void Serializer_UnknownElement(object sender, XmlElementEventArgs e)
{
    var group = (DeactivationsGroup)e.ObjectBeingDeserialized;
    group.GroupNames.Add(e.Element.Name, new GroupName { Name = e.Element.InnerText });
}

Fiddle.

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-10-08
    • 1970-01-01
    相关资源
    最近更新 更多