在实施IXmlSerializable 时,您需要遵守Marc Gravell 的this answer 到Proper way to implement IXmlSerializable? 中所述的规则以及文档:
对于IXmlSerializable.WriteXml(XmlWriter):
您提供的WriteXml 实现应该写出对象的XML 表示。框架编写一个包装器元素并在 XML 编写器启动后定位它。您的实现可能会编写其内容,包括子元素。然后框架关闭包装元素。
对于IXmlSerializable.ReadXml(XmlReader):
ReadXml 方法必须使用 WriteXml 方法写入的信息重构您的对象。
调用此方法时,阅读器将定位在包装您的类型信息的开始标记上。也就是说,直接在指示序列化对象开始的开始标记上。当此方法返回时,它必须从头到尾读取整个元素,包括其所有内容。与WriteXml 方法不同,框架不会自动处理包装元素。您的实施必须这样做。未能遵守这些定位规则可能会导致代码生成意外的运行时异常或损坏的数据。
事实证明,编写一个ReadXml() 来正确处理诸如乱序或意外元素、缺少或额外的空白、空元素等边缘情况是非常棘手的。因此,采用某种解析框架来正确遍历 XML 树是有意义的,例如 Why does XmlSerializer throws an Exception and raise a ValidationEvent when a schema validation error occurs inside IXmlSerializable.ReadXml() 中的this one,并将其扩展为处理注释节点:
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;
}
}
}
然后修改你的类来使用它:
[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。
注意事项:
由于您的类是密封的,我将使用反射替换为直接访问要序列化的属性。
-
在您的版本中,您可以通过写入 ToString() 值将 T Value 序列化为 XML:
writer.WriteElementString(propertyInfo.Name, propertyInfo.GetValue(this, null)?.ToString());
除非值本身是一个字符串,否则这很可能会产生错误的结果:
为避免这些问题,我的版本通过构造适当的XmlSerializer 将值序列化为 XML。这保证了正确性,但可能比您的版本慢。如果这里的性能很重要,您可以检查已知类型(例如string)并手动将它们格式化为 XML,例如使用实用程序类XmlConvert。
XmlReader.ReadSubtree() 用于确保XmlReader 不会被HandleXmlElement(XmlReader reader) 错误定位。
演示小提琴here.