使用自定义 IXmlSerializer 反序列化注释 [英] Deserialize Comments using Custom IXmlSerializer

查看:36
本文介绍了使用自定义 IXmlSerializer 反序列化注释的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试将我的 Description 属性序列化为 Xml 注释.所以,为了做到这一点,我实现了 IXmlSerializable,下面的 WriteXml 生成了非常好的 XML.

I am attempting to serialize my Description property to an Xml comment. So, to do this I have implemented IXmlSerializable and the following WriteXml produces very nice XML.

[Serializable]
public sealed class Setting<T> : SettingBase, IXmlSerializable
{
    public Setting() { }

    public Setting(T value, string description)
    {
        Value = value;
        Description = description;
    }

    public Setting(string command, T value, string description)
        : this(value, description)
    {
        Command = command;
    }

    public XmlSchema GetSchema()
    {
        return null;
    }

    public void ReadXml(XmlReader reader)
    {
    }

    public void WriteXml(XmlWriter writer)
    {
        var properties = GetType().GetProperties();
        foreach (var propertyInfo in properties)
        {
            if (propertyInfo.IsDefined(typeof(XmlCommentAttribute), false))
                writer.WriteComment(Description);
            else if (!propertyInfo.CustomAttributes.Any((attr) => attr.AttributeType.Equals(typeof(XmlIgnoreAttribute))))
                writer.WriteElementString(propertyInfo.Name, propertyInfo.GetValue(this, null)?.ToString());
        }
    }

    [XmlComment, Browsable(false)]
    public string Description { get; set; }

    [XmlElement, Browsable(false)]
    public string Command { get; set; }

    [XmlElement, Browsable(false)]
    public T Value { get; set; }

    [XmlIgnore]
    public override object ValueUntyped { get { return Value; } }
}

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class XmlCommentAttribute : Attribute {}

但是,我曾多次尝试实现 ReadXml,但我似乎无法反序列化 Description 注释.

However, I have had many attempts at implementing the ReadXml but i cannot seem to be able to deserialize the Description comment.

如何实现 ReadXml 来使我的类脱轨?

How can I implement ReadXml to deserailize my class?

推荐答案

在实施 IXmlSerializable 时,您需要遵守 这个答案 实现 IXmlSerializable 的正确方法? Marc Gravell 以及文档:

When implementing IXmlSerializable you need to adhere to the rules stated in this answer to Proper way to implement IXmlSerializable? by Marc Gravell as well as the documentation:

对于 IXmlSerializable.WriteXml(XmlWriter):

您提供的 WriteXml 实现应该写出对象的 XML 表示.框架编写一个包装元素并在它开始后定位 XML 编写器.您的实现可能会编写其内容,包括子元素.然后框架关闭包装元素.

The WriteXml implementation you provide should write out the XML representation of the object. The framework writes a wrapper element and positions the XML writer after its start. Your implementation may write its contents, including child elements. The framework then closes the wrapper element.

对于 IXmlSerializable.ReadXml(XmlReader):

ReadXml 方法必须使用 WriteXml 方法写入的信息重构您的对象.

The ReadXml method must reconstitute your object using the information that was written by the WriteXml method.

当这个方法被调用时,阅读器被定位在包装你的类型信息的开始标签上.也就是说,直接在指示序列化对象开始的开始标记上.当此方法返回时,它必须从头到尾读取了整个元素,包括其所有内容.与 WriteXml 方法不同,框架不会自动处理包装元素.您的实现必须这样做.不遵守这些定位规则可能会导致代码生成意外的运行时异常或损坏的数据.

When this method is called, the reader is positioned on the start tag that wraps the information for your type. That is, directly on the start tag that indicates the beginning of a serialized object. When this method returns, it must have read the entire element from beginning to end, including all of its contents. Unlike the WriteXml method, the framework does not handle the wrapper element automatically. Your implementation must do so. Failing to observe these positioning rules may cause code to generate unexpected runtime exceptions or corrupt data.

事实证明,编写一个 ReadXml() 来正确处理边缘情况非常棘手,例如乱序或意外元素、缺失或多余空格、空元素等.因此,采用某种解析框架来正确遍历 XML 树是有意义的,例如 这个来自当 IXmlSerializable.ReadXml() 内部发生架构验证错误时,为什么 XmlSerializer 会抛出异常并引发 ValidationEventem>,并扩展它来处理评论节点:

It turns out to be very tricky to write a ReadXml() that correctly handles edge cases such as out-of-order or unexpected elements, missing or extra whitespace, empty elements, and so on. As such it makes sense to adopt some sort of parsing framework to iterate through the XML tree correctly, such as this one from Why does XmlSerializer throws an Exception and raise a ValidationEvent when a schema validation error occurs inside IXmlSerializable.ReadXml(), and extend it to handle comment nodes:

public static class XmlSerializationExtensions
{
    // Adapted from this answer https://stackoverflow.com/a/60498500/3744182
    // To https://stackoverflow.com/questions/60449088/why-does-xmlserializer-throws-an-exception-and-raise-a-validationevent-when-a-sc
    // by handling comments.
    public static void ReadIXmlSerializable(XmlReader reader, Func<XmlReader, bool> handleXmlAttribute, Func<XmlReader, bool> handleXmlElement, Func<XmlReader, bool> handleXmlText, Func<XmlReader, bool> handleXmlComment)
    {
        //https://docs.microsoft.com/en-us/dotnet/api/system.xml.serialization.ixmlserializable.readxml?view=netframework-4.8#remarks
        //When this method is called, the reader is positioned on the start tag that wraps the information for your type. 
        //That is, directly on the start tag that indicates the beginning of a serialized object. 
        //When this method returns, it must have read the entire element from beginning to end, including all of its contents. 
        //Unlike the WriteXml method, the framework does not handle the wrapper element automatically. Your implementation must do so. 
        //Failing to observe these positioning rules may cause code to generate unexpected runtime exceptions or corrupt data.
        reader.MoveToContent();
        if (reader.NodeType != XmlNodeType.Element)
            throw new XmlException(string.Format("Invalid NodeType {0}", reader.NodeType));
        if (reader.HasAttributes)
        {
            for (int i = 0; i < reader.AttributeCount; i++)
            {
                reader.MoveToAttribute(i);
                handleXmlAttribute(reader);
            }
            reader.MoveToElement(); // Moves the reader back to the element node.
        }
        if (reader.IsEmptyElement)
        {
            reader.Read();
            return;
        }
        reader.ReadStartElement(); // Advance to the first sub element of the wrapper element.
        while (reader.NodeType != XmlNodeType.EndElement)
        {
            if (reader.NodeType == XmlNodeType.Element)
            {
                using (var subReader = reader.ReadSubtree())
                {
                    subReader.MoveToContent();
                    handleXmlElement(subReader);
                }
                // ReadSubtree() leaves the reader positioned ON the end of the element, so read that also.
                reader.Read();
            }
            else if (reader.NodeType == XmlNodeType.Text || reader.NodeType == XmlNodeType.CDATA)
            {
                var type = reader.NodeType;
                handleXmlText(reader);
                // Ensure that the reader was not advanced.
                if (reader.NodeType != type)
                    throw new XmlException(string.Format("handleXmlText incorrectly advanced the reader to a new node {0}", reader.NodeType));
                reader.Read();
            }
            else if (reader.NodeType == XmlNodeType.Comment)
            {
                var type = reader.NodeType;
                handleXmlComment(reader);
                // Ensure that the reader was not advanced.
                if (reader.NodeType != type)
                    throw new XmlException(string.Format("handleXmlComment incorrectly advanced the reader to a new node {0}", reader.NodeType));
                reader.Read();
            }
            else // Whitespace, etc.
            {
                // Skip() leaves the reader positioned AFTER the end of the node.
                reader.Skip();
            }
        }
        // Move past the end of the wrapper element
        reader.ReadEndElement();
    }

    public static void ReadIXmlSerializable(XmlReader reader, Func<XmlReader, bool> handleXmlAttribute, Func<XmlReader, bool> handleXmlElement, Func<XmlReader, bool> handleXmlText)
    {
        ReadIXmlSerializable(reader, handleXmlAttribute, handleXmlElement, handleXmlText, r => true);
    }

    public static void WriteIXmlSerializable(XmlWriter writer, Action<XmlWriter> writeAttributes, Action<XmlWriter> writeNodes)
    {
        //https://docs.microsoft.com/en-us/dotnet/api/system.xml.serialization.ixmlserializable.writexml?view=netframework-4.8#remarks
        //The WriteXml implementation you provide should write out the XML representation of the object. 
        //The framework writes a wrapper element and positions the XML writer after its start. Your implementation may write its contents, including child elements. 
        //The framework then closes the wrapper element.
        writeAttributes(writer);
        writeNodes(writer);
    }
}

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

然后修改您的类以使用它,如下所示:

Then modify your class to use it as follows:

[Serializable]
public sealed class Setting<T> : SettingBase, IXmlSerializable
{
    public Setting() { }

    public Setting(T value, string description)
    {
        Value = value;
        Description = description;
    }

    public Setting(string command, T value, string description)
        : this(value, description)
    {
        Command = command;
    }

    public XmlSchema GetSchema() { return null;}

    public void ReadXml(XmlReader reader)
    {
        XmlSerializationExtensions.ReadIXmlSerializable(reader, r => true,
            r =>
            {
                switch (r.LocalName)
                {
                    case "Command":
                        Command = r.ReadElementContentAsString();
                        break;
                    case "Value":
                        var serializer = XmlSerializerFactory.Create(typeof(T), "Value", reader.NamespaceURI);
                        Value = (T)serializer.Deserialize(r);
                        break;
                }
                return true;
            },
            r => true, r => { Description += r.Value; return true; });
    }

    public void WriteXml(XmlWriter writer)
    {
        XmlSerializationExtensions.WriteIXmlSerializable(writer, w => { },
            w =>
            {
                if (Description != null)
                    w.WriteComment(Description);
                if (Command != null)
                    w.WriteElementString("Command", Command);
                if (Value != null)
                {
                    var serializer = XmlSerializerFactory.Create(typeof(T), "Value", null);
                    serializer.Serialize(w, Value);
                }
            });
    }

    public string Description { get; set; }

    public string Command { get; set; }

    public T Value { get; set; }

    public override object ValueUntyped { get { return Value; } }
}

// ABSTRACT BASE CLASS NOT INCLUDED IN QUESTION, THIS IS JUST A GUESS
[Serializable]
public abstract class SettingBase
{
    public abstract object ValueUntyped { get; }
}

并且您将能够将其往返传输到 XML.

And you will be able to round-trip it to XML.

注意事项:

  • 由于您的类是密封的,因此我将反射的使用替换为直接访问要序列化的属性.

  • Since your class is sealed I replaced use of reflection with direct access to properties to serialize.

在您的版本中,您通过写入 ToString() 值将 T 值 序列化为 XML:

In your version you serialize your T Value to XML by writing its ToString() value:

writer.WriteElementString(propertyInfo.Name, propertyInfo.GetValue(this, null)?.ToString());

除非值本身是一个字符串,否则这很可能会产生错误的结果:

Unless the value is, itself, a string, this is likely to produce a wrong result:

  • 数字、DateTimeTimeSpan 和类似的原语将被本地化.XML 原语应始终以文化上不变的方式进行格式化.

  • Numeric, DateTime, TimeSpan and similar primitives will be localized. XML primitives should always be formatted in a culturally invariant manner.

复杂对象,如 string [] 不覆盖 ToString() 的格式将完全不正确.

Complex objects such as string [] that do not override ToString() will be formatted in a completely incorrect manner.


为了避免这些问题,我的版本通过构造适当的XmlSerializer 将值序列化为 XML.这保证了正确性,但可能比您的版本慢.如果性能在这里很重要,您可以检查已知类型(例如 string)并手动将它们格式化为 XML,例如使用实用程序类 XmlConvert.


To avoid these problems my version serializes the value to XML by constructing an appropriate XmlSerializer. This guarantees correctness but may be slower than your version. If performance matters here you could check for known types (such as string) and format them to XML manually, using e.g. the utility class XmlConvert.

XmlReader.ReadSubtree() 用于确保 XmlReader 不会被 HandleXmlElement(XmlReader reader) 错误定位.

XmlReader.ReadSubtree() is used to ensure that the XmlReader is not mispositioned by HandleXmlElement(XmlReader reader).

演示小提琴 此处.

这篇关于使用自定义 IXmlSerializer 反序列化注释的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆