您的问题是您有一系列元素,如下所示:
<field name="min-longitude" data="-67.55643521" kind="double"/>
并且您希望将它们解释为多态地指定不同类型的值,具体取决于kind 属性的值。
不幸的是,开箱即用的XmlSerializer 不支持使用kind 等任意属性的多态反序列化。相反,它支持使用 w3c 标准属性 xsi:type 来指定元素类型,如 docs 中所述。
但是,如果我们简单地将属性data 视为一个字符串,则每个<field> 元素的XML 实际上都有一个固定的模式,unit 属性是可选的。因此,您可以将您的 XML 反序列化为以下 DTO,然后将字段值转换为其预期类型:
public abstract class RPPItemBase
{
[XmlAttribute("name")]
public string Name { get; set; }
[XmlAttribute("kind")]
public string Kind { get; set; }
bool ShouldSerializeKind() { return !string.IsNullOrEmpty(Kind); }
}
[XmlRoot("object")]
public class RPPProjectDTO : RPPItemBase
{
public RPPProjectDTO() { this.Kind = "project"; }
[XmlAttribute("states")]
public string States { get; set; }
[XmlElement("fields")]
public RPPProjectFieldsDTO Fields { get; set; }
}
[XmlRoot("fields")]
public class RPPProjectFieldsDTO
{
[XmlElement("field")]
public RPPProjectFieldDTO[] Fields { get; set; }
}
public class RPPProjectFieldDTO : RPPItemBase
{
[XmlAttribute("data")]
public string Data { get; set; }
[XmlAttribute("unit")]
public string Unit { get; set; }
public bool ShouldSerializeUnit() { return !string.IsNullOrEmpty(Unit); }
}
示例fiddle #1。
但是,从您的问题来看,您似乎希望在序列化中使用某种(半)自动方式将 c# 对象的类型化属性从 <field name =...> 元素列表转换为 <field name =...> 元素列表。由于开箱即用不支持此功能,因此您需要将代理 RPPProjectFieldDTO [] 属性添加到您希望以这种方式序列化的每种类型,并处理属性 getter 和 setter 内的转换。以下是一个原型实现:
[XmlRoot("object")]
public class RPPProject : RPPItemBase
{
public RPPProject() { this.Kind = "project"; }
[XmlAttribute("states")]
public string States { get; set; }
[XmlElement("fields")]
public RPPProjectFields Fields { get; set; }
}
[XmlRoot("fields")]
[DataContract(Name = "fields")]
public class RPPProjectFields
{
[XmlIgnore]
[DataMember(Name = "coordinate-system-internal")]
public string CoordinateSystemInternal { get; set; }
[XmlIgnore]
[DataMember(Name = "min-longitude")]
public double MinLongitude { get; set; }
[XmlIgnore]
[DataMember(Name = "min-latitude")]
public double MinLatitude { get; set; }
[XmlIgnore]
[DataMember(Name = "min-altitude")]
public DimensionalValue MinAltitude { get; set; }
[XmlIgnore]
[DataMember(Name = "max-longitude")]
public double MaxLongitude { get; set; }
[XmlIgnore]
[DataMember(Name = "max-latitude")]
public double MaxLatitude { get; set; }
[XmlIgnore]
[DataMember(Name = "max-altitude")]
public DimensionalValue MaxAltitude { get; set; }
[XmlIgnore]
[DataMember(Name = "pop")]
public double[][] Pop { get; set; } //[4][4]
[XmlIgnore]
[DataMember(Name = "pop-acquisition")]
public double[][] PopAcquisition { get; set; } //[4][4]
[XmlElement("field")]
public RPPProjectFieldDTO[] Fields
{
get
{
return this.GetDataContractFields();
}
set
{
this.SetDataContractFields(value);
}
}
}
public enum Units
{
[XmlEnum("none")]
None,
[XmlEnum("m")]
Meters,
[XmlEnum("cm")]
Centimeters,
[XmlEnum("mm")]
Millimeters,
}
// Going with something like the Money Pattern here:
// http://www.dsc.ufcg.edu.br/~jacques/cursos/map/recursos/fowler-ap/Analysis%20Pattern%20Quantity.htm
// You may want to implement addition, subtraction, comparison and so on.
public struct DimensionalValue
{
readonly Units units;
readonly double value;
public DimensionalValue(Units units, double value)
{
this.units = units;
this.value = value;
}
public Units Units { get { return units; } }
public double Value { get { return value; } }
}
public interface IFieldDTOParser
{
Regex Regex { get; }
bool TryCreateDTO(object obj, out RPPProjectFieldDTO field);
object Parse(RPPProjectFieldDTO field, Match match);
}
class StringParser : IFieldDTOParser
{
readonly Regex regex = new Regex("^string$", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.CultureInvariant);
#region IFieldDTOParser Members
public Regex Regex { get { return regex; } }
public bool TryCreateDTO(object obj, out RPPProjectFieldDTO field)
{
if (obj is string)
{
field = new RPPProjectFieldDTO { Data = (string)obj, Kind = "string"};
return true;
}
field = null;
return false;
}
public object Parse(RPPProjectFieldDTO field, Match match)
{
return field.Data;
}
#endregion
}
class DoubleParser : IFieldDTOParser
{
readonly Regex regex = new Regex("^double$", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.CultureInvariant);
#region IFieldDTOParser Members
public Regex Regex { get { return regex; } }
public bool TryCreateDTO(object obj, out RPPProjectFieldDTO field)
{
if (obj is double)
{
field = new RPPProjectFieldDTO { Data = XmlConvert.ToString((double)obj), Kind = "double"};
return true;
}
else if (obj is DimensionalValue)
{
var value = (DimensionalValue)obj;
field = new RPPProjectFieldDTO { Data = XmlConvert.ToString(value.Value), Kind = "double", Unit = value.Units.ToXmlValue() };
return true;
}
field = null;
return false;
}
public object Parse(RPPProjectFieldDTO field, Match match)
{
var value = XmlConvert.ToDouble(field.Data);
if (string.IsNullOrEmpty(field.Unit))
return value;
var unit = field.Unit.FromXmlValue<Units>();
return new DimensionalValue(unit, value);
}
#endregion
}
class Double2DArrayParser : IFieldDTOParser
{
readonly Regex regex = new Regex("^double\\[([0-9]+)\\]\\[([0-9]+)\\]$", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.CultureInvariant);
#region IFieldDTOParser Members
public Regex Regex { get { return regex; } }
public bool TryCreateDTO(object obj, out RPPProjectFieldDTO field)
{
if (obj is double[][])
{
var value = (double[][])obj;
var nCols = value.GetLength(0);
var rowLengths = value.Select(a => a == null ? 0 : a.Length).Distinct().ToArray();
if (rowLengths.Length == 0)
{
field = new RPPProjectFieldDTO { Data = "", Kind = string.Format("double[{0}][{1}]", XmlConvert.ToString(nCols), "0")};
return true;
}
else if (rowLengths.Length == 1)
{
field = new RPPProjectFieldDTO
{
Data = String.Join(" ", value.SelectMany(a => a).Select(v => XmlConvert.ToString(v))),
Kind = string.Format("double[{0}][{1}]", XmlConvert.ToString(nCols), XmlConvert.ToString(rowLengths[0]))
};
return true;
}
}
field = null;
return false;
}
public object Parse(RPPProjectFieldDTO field, Match match)
{
var nRows = XmlConvert.ToInt32(match.Groups[1].Value);
var nCols = XmlConvert.ToInt32(match.Groups[2].Value);
var array = new double[nRows][];
var values = field.Data.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
for (int iRow = 0, iValue = 0; iRow < nRows; iRow++)
{
array[iRow] = new double[nCols];
for (int iCol = 0; iCol < nCols; iCol++)
{
if (iValue < values.Length)
array[iRow][iCol] = XmlConvert.ToDouble(values[iValue++]);
}
}
return array;
}
#endregion
}
public static class FieldDTOExtensions
{
readonly static IFieldDTOParser[] parsers = new IFieldDTOParser[]
{
new StringParser(),
new DoubleParser(),
new Double2DArrayParser(),
};
public static void SetDataContractFields<T>(this T @this, RPPProjectFieldDTO [] value)
{
if (value == null)
return;
var lookup = value.ToDictionary(f => f.Name, f => f.Parse<object>());
var query = from p in @this.GetType().GetProperties()
where p.CanRead && p.CanWrite && p.GetIndexParameters().Length == 0
let a = p.GetCustomAttributes<DataMemberAttribute>().SingleOrDefault()
where a != null
select new { Property = p, Name = a.Name };
foreach (var property in query)
{
object item;
if (lookup.TryGetValue(property.Name, out item))
{
property.Property.SetValue(@this, item, null);
}
}
}
public static RPPProjectFieldDTO[] GetDataContractFields<T>(this T @this)
{
var query = from p in @this.GetType().GetProperties()
where p.CanRead && p.CanWrite && p.GetIndexParameters().Length == 0
let a = p.GetCustomAttributes<DataMemberAttribute>().SingleOrDefault()
where a != null
let v = p.GetValue(@this, null)
where v != null
select FieldDTOExtensions.ToDTO(v, a.Name);
return query.ToArray();
}
public static T Parse<T>(this RPPProjectFieldDTO field)
{
foreach (var parser in parsers)
{
var match = parser.Regex.Match(field.Kind);
if (match.Success)
{
return (T)parser.Parse(field, match);
}
}
throw new ArgumentException(string.Format("Unsupported object {0}", field.Kind));
}
public static RPPProjectFieldDTO ToDTO(object obj, string name)
{
RPPProjectFieldDTO field;
foreach (var parser in parsers)
{
if (parser.TryCreateDTO(obj, out field))
{
field.Name = name;
return field;
}
}
throw new ArgumentException(string.Format("Unsupported object {0}", obj));
}
}
// Taken from
// https://stackoverflow.com/questions/42990069/get-element-of-an-enum-by-sending-xmlenumattribute-c
public static partial class XmlExtensions
{
static XmlExtensions()
{
noStandardNamespaces = new XmlSerializerNamespaces();
noStandardNamespaces.Add("", ""); // Disable the xmlns:xsi and xmlns:xsd attributes.
}
readonly static XmlSerializerNamespaces noStandardNamespaces;
internal const string RootNamespace = "XmlExtensions";
internal const string RootName = "Root";
public static TEnum FromXmlValue<TEnum>(this string xml) where TEnum : struct, IConvertible, IFormattable
{
var element = new XElement(XName.Get(RootName, RootNamespace), xml);
return element.Deserialize<XmlExtensionsEnumWrapper<TEnum>>(null).Value;
}
public static T Deserialize<T>(this XContainer element, XmlSerializer serializer)
{
using (var reader = element.CreateReader())
{
object result = (serializer ?? new XmlSerializer(typeof(T))).Deserialize(reader);
if (result is T)
return (T)result;
}
return default(T);
}
public static string ToXmlValue<TEnum>(this TEnum value) where TEnum : struct, IConvertible, IFormattable
{
var root = new XmlExtensionsEnumWrapper<TEnum> { Value = value };
return root.SerializeToXElement().Value;
}
public static XElement SerializeToXElement<T>(this T obj)
{
return obj.SerializeToXElement(null, noStandardNamespaces); // Disable the xmlns:xsi and xmlns:xsd attributes by default.
}
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;
}
}
[XmlRoot(XmlExtensions.RootName, Namespace = XmlExtensions.RootNamespace)]
[XmlType(IncludeInSchema = false)]
public class XmlExtensionsEnumWrapper<TEnum>
{
[XmlText]
public TEnum Value { get; set; }
}
注意事项:
-
RPPProjectFields 的常规类型属性都标有[XmlIgnore]。相反,只有一个代理属性
public RPPProjectFieldDTO[] Fields { get { ... } set { ... } }
这是序列化的。
在代理属性中,反射用于循环遍历RPPProjectFields 的所有“常规”属性,并将它们转换为RPPProjectFieldDTO 类型的对象。然而,XML 名称(例如 "max-longitude")包含一个字符 -,该字符在 c# 标识符中使用是非法的。因此有必要指定一个备用名称,但属性不能用[XmlElement("Alternate Name")] 标记,因为它们已经用[XmlIgnore] 标记。所以我改用data contract attributes 来指定备用名称。
您的某些 double 值具有单位,例如<field name="min-altitude" data="550.094" kind="double" unit="m"/> 为了解决这个问题,我引入了一个容器结构 DimensionalValue。我们的想法是使用此结构遵循quantity pattern(有时称为money pattern)。
示例 fiddle #2 无法编译,因为 .NET Fiddle 不支持 System.Runtime.Serialization.dll。