2017-11-02 2 views
0

Ich habe versucht, eine seltsam konstruierte XML-Datei zu lesen und habe gegen eine Wand gestoßen. Ich muss das XML in Datenstrukturen in meinem C# -Programm deserialisieren. Eine Probe des XML-Seins:C# Xml deserialize XmlAttribute nach Wert des Attributs

<object name="CAU_17_163" kind="project" states="expanded"> 
    <fields> 
    <field name="coordinate-system-internal" data="WGS84" kind="string"/> 
    <field name="min-longitude" data="-67.55643521" kind="double"/> 
    <field name="min-latitude" data="45.09374232" kind="double"/> 
    <field name="min-altitude" data="550.094" kind="double" unit="m"/> 
    <field name="max-longitude" data="-66.52992272" kind="double"/> 
    <field name="max-latitude" data="45.86876855" kind="double"/> 
    <field name="max-altitude" data="1400.954" kind="double" unit="m"/> 
    <field name="pop" data="0.917266016 0.398275101 0.000000000 0.000000000 -0.285868639 0.658383080 0.696283592 0.000000000 0.277312418 -0.638677277 0.717766786 0.000000000 1771794.580394641 -4080614.005124380 4555229.910096285 1.000000000 " kind="double[4][4]"/> 
    <field name="pop-acquisition" data="0.917266016 0.398275101 0.000000000 0.000000000 -0.285868639 0.658383080 0.696283592 0.000000000 0.277312418 -0.638677277 0.717766786 0.000000000 1771794.580394641 -4080614.005124380 4555229.910096285 1.000000000 " kind="double[4][4]"/> 
    </fields> 

Und meine C# Strukturen sind wie folgt:

Eine Klasse, mir zu erlauben, den Wert der "Daten" Attribut zu greifen

public class Data<T> 
    { 
     T dataAttr; 

     [XmlAttribute("data")] 
     public T DataAttr { get => dataAttr; set => dataAttr = value; } 
    } 

Eine Struktur, die das gesamte "Projekt" -Objekt enthält

[XmlRoot("object")] 
    public struct RPPProject 
    { 
     string name; 
     RPPProjectFields fields; 

     [XmlAttribute("name")] 
     public string Name { get => name; set => name = value; } 
     [XmlAttribute("fields")] 
     public RPPProjectFields Fields { get => fields; set => fields = value; } 
    } 

Eine Struktur, die die Felder innerhalb des dem Tag enthalten

[XmlRoot("fields")] 
    public struct RPPProjectFields 
    { 
     //Project fields 
     Data<string> coordinate_system_internal; 
     Data<double> min_longitude; 
     Data<double> min_latitude; 
     Data<double> min_altitude; 
     Data<double> max_longitude; 
     Data<double> max_latitude; 
     Data<double> max_altitude; 
     Data<double[]> pop; //[4][4] 
     Data<double[]> pop_acquisition; //[4][4] 

     [XmlElement(ElementName = "coordinate-system-internal")] 
     public Data<string> Coordinate_system_internal { get => coordinate_system_internal; set => coordinate_system_internal = value; } 
     [XmlElement(ElementName = "min-longitude")] 
     public Data<double> Min_longitude { get => min_longitude; set => min_longitude = value; } 
     [XmlElement(ElementName = "min-latitude")] 
     public Data<double> Min_latitude { get => min_latitude; set => min_latitude = value; } 
     [XmlElement(ElementName = "min-altitude")] 
     public Data<double> Min_altitude { get => min_altitude; set => min_altitude = value; } 
     [XmlElement(ElementName = "max-longitude")] 
     public Data<double> Max_longitude { get => max_longitude; set => max_longitude = value; } 
     [XmlElement(ElementName = "max-latitude")] 
     public Data<double> Max_latitude { get => max_latitude; set => max_latitude = value; } 
     [XmlElement(ElementName = "max-altitude")] 
     public Data<double> Max_altitude { get => max_altitude; set => max_altitude = value; } 
     [XmlElement(ElementName = "pop")] 
     public Data<double[]> Pop { get => pop; set => pop = value; } 
     [XmlElement(ElementName = "pop-acquisition")] 
     public Data<double[]> Pop_acquisition { get => pop_acquisition; set => pop_acquisition = value; } 
    } 

Das Problem ist, dass das Attribut „name“ von der nicht, was als der Name des Knotens, und daher ist die Syntax von [XmlElement registriert ist (ElementName = "Koordinatensystem-intern")] funktioniert nicht. Ich bin ratlos. Grundsätzlich muss ich in der Lage sein, den Wert des "name" -Attributs als die Art der Deserialisierung in die verschiedenen Projektfelder anzugeben und den Wert "data" innerhalb der angegebenen Variablen zu speichern.

+0

Kennen Sie die Menge der '' Elemente im Voraus, oder möchten Sie eine flexible Struktur, die eine beliebige Sammlung von Feldern ermöglicht? – dbc

+0

In dem speziellen Fall oben kenne ich sie im Voraus; aber weiter im XML wäre eine flexible Struktur erforderlich. – MRoscoe

Antwort

0

Ihr Problem ist, dass Sie eine Folge von Elementen wie die folgenden haben:

<field name="min-longitude" data="-67.55643521" kind="double"/> 

Und Sie würden sie gerne als polymorph Angabe unterschiedliche Arten von Werten in Abhängigkeit von dem Wert des kind Attributs interpretieren.

Leider unterstützt XmlSerializerXmlSerializer nicht polymorphe Deserialisierung mit einem beliebigen Attribut wie kind. Stattdessen unterstützt es die Verwendung des w3c-Standardattributs xsi:type, um den Elementtyp anzugeben, wie in der docs erläutert.

Wenn wir jedoch einfach das Attribut data betrachten eine Zeichenfolge zu sein, die XML für jedes <field> Element hat tatsächlich ein festes Schema, mit dem unit Attribute optional sein. So könnten Sie Ihren XML-Code in die folgenden DTO deserialisieren und dann später die Feldwerte in ihre erwarteten Typen konvertieren:

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

Probe fiddle #1.

Aus Ihrer Frage scheint jedoch, Sie möchten einige (semi-) automatische Möglichkeit innerhalb der Serialisierung typisierte Eigenschaften eines C# -Objekts von und zu einer Liste von <field name =...> Elementen konvertieren. Da dies nicht standardmäßig unterstützt wird, müssen Sie jedem Typ, den Sie auf diese Weise serialisieren möchten, eine Ersatz-RPPProjectFieldDTO []-Eigenschaft hinzufügen und die Konvertierungen in den Eigenschaften-Gettern und -Einstellungen durchführen. In der folgenden wäre eine Prototyp-Implementierung sein:

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

Hinweise:

  • Die herkömmlichen, typisierte Eigenschaften von RPPProjectFields sind alle gekennzeichnet mit [XmlIgnore].Stattdessen gibt es eine einzige Ersatzeigenschaft

    , das serialisiert wird.

  • Innerhalb der Surrogate-Eigenschaft wird die Reflektion verwendet, um alle "regulären" Eigenschaften von RPPProjectFields zu durchlaufen und sie in Objekte des Typs RPPProjectFieldDTO umzuwandeln. Die XML-Namen (z. B. "max-longitude") enthalten jedoch ein Zeichen -, das zur Verwendung in einem C# -Kennzeichner unzulässig ist. Daher ist es notwendig, einen alternativen Namen anzugeben, aber die Eigenschaften können nicht mit [XmlElement("Alternate Name")] markiert werden, da sie bereits mit [XmlIgnore] gekennzeichnet sind. Also habe ich statt die alternativen Namen angegeben.

  • Einige Ihrer double Werte haben Einheiten, z. <field name="min-altitude" data="550.094" kind="double" unit="m"/> Um dies zu handhaben, habe ich eine Container-Struktur DimensionalValue eingeführt. Die Idee wäre, der quantity pattern (manchmal money pattern genannt) zu folgen, die diese Struktur verwendet.

Probe fiddle #2, die leider nicht kompilieren, weil .NET Fiddle nicht System.Runtime.Serialization.dll unterstützen.