2

I have the following test XML string:

<?xml version="1.0" encoding="UTF-8"?>
<test id="myid">
 <b>b1</b>
 <a>a2</a>
 <a>a1</a>
 <b>b2</b>
</test>

which I deserialize using this class:

[XmlRoot(ElementName = "test")]
public class Test
{
    [XmlElement(ElementName = "a")]
    public List<string> A { get; set; }
    [XmlElement(ElementName = "b")]
    public List<string> B { get; set; }
    [XmlAttribute(AttributeName = "id")]
    public string Id { get; set; }
}

If I'm now going to serialize the object the result will be:

<?xml version="1.0" encoding="utf-16"?>
<test xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" id="myid">
  <a>a2</a>
  <a>a1</a>
  <b>b1</b>
  <b>b2</b>
</test>

Is there a way to keep the initial sort order? I guess I can't use [XmlElementAttribute(Order = x)] cause the order shouldn't be hardcoded but identically with the initial xml.

I thought about adding an order property to my lists like that

[XmlRoot(ElementName="a")]
public class A 
{
    [XmlAttribute(AttributeName="order")]
    public string Order { get; set; }
    [XmlText]
    public string Text { get; set; }
}

[XmlRoot(ElementName="b")]
public class B 
{
    [XmlAttribute(AttributeName="order")]
    public string Order { get; set; }
    [XmlText]
    public string Text { get; set; }
}

[XmlRoot(ElementName="test")]
public class Test 
{
    [XmlElement(ElementName="a")]
    public List<A> A { get; set; }
    [XmlElement(ElementName="b")]
    public List<B> B { get; set; }
    [XmlAttribute(AttributeName="id")]
    public string Id { get; set; }
}

but I don't know how to order by them when serializing.

dbc
  • 104,963
  • 20
  • 228
  • 340
CrazyTea
  • 297
  • 1
  • 3
  • 13
  • Out of curiosity, is there a reason you need the XML to be in a particular order? The sort, in my opinion, would only be used once you have the data loaded or deserialized back into your object(s). – gmiley Jan 04 '18 at 13:54
  • @gmiley I'm developing an Office VSTO AddIn which allows you to add your own ribbon tab with UI elements defined in XML. The xml element order represents the ribbon UI order (for example: Save Button - Close Button OR Close Button - Save Button ) – CrazyTea Jan 04 '18 at 14:01
  • If you think about it - `Test` class does not contain any information about "original" order, so there is no way to do that. – Evk Jan 04 '18 at 14:05
  • Then you would go with your idea of adding a sort property/ element and perform the sorting on the collection. Forget about the order that your xml is in. – gmiley Jan 04 '18 at 14:09
  • @gmiley Just sorting a and b wouldn't make it possible to have one element of b before a (as in my example). – CrazyTea Jan 04 '18 at 14:14
  • You would need to redesign your classes. – gmiley Jan 04 '18 at 14:15

2 Answers2

3

You can do this with XmlSerializer by using a single collection to capture both the <a> and <b> elements and applying the [XmlElement(Name, Type = typeof(...))] attribute to it multiple times, once for each desired element name. Because you are using a single collection to deserialize both elements, the order is automatically preserved. However, to make this work, XmlSerializer must be able to determine the correct element name when re-serializing. There are two approaches to accomplish this, as documented in Choice Element Binding Support:

  1. If the collection contains polymorphic items, the element name can be mapped to the concrete item type by using the [XmlElementAttribute(String, Type)] constructor. For instance, if you have a sequence of elements that might be strings or integers like so:

    <Things>
       <string>Hello</string>
       <int>999</int>
    </Things>
    

    This can be bound to a collection as follows:

    public class Things
    {
        [XmlElement(Type = typeof(string)),
        XmlElement(Type = typeof(int))]
        public List<object> StringsAndInts { get; set; }
    }
    
  2. If the collection contains only a single type of item, the element name can be encoded in an associated array of enum values, where the enum names correspond to the element names and the array itself is identified via the [XmlChoiceIdentifierAttribute] attribute.

    For details, see the documentation examples.

I find option #1 easier to work with than option #2. Using this approach, the following model will deserialize and re-serialize your XML while successfully preserving the order of the <a> and <b> elements:

public abstract class StringElementBase
{
    [XmlText]
    public string Text { get; set; }

    public static implicit operator string(StringElementBase element)
    {
        return element == null ? null : element.Text;
    }
}

public sealed class A : StringElementBase
{
}

public sealed class B : StringElementBase
{
}

[XmlRoot(ElementName = "test")]
public class Test
{
    [XmlElement("a", Type = typeof(A))]
    [XmlElement("b", Type = typeof(B))]
    public List<StringElementBase> Items { get; } = new List<StringElementBase>();

    [XmlIgnore]
    // For convenience, enumerate through the string values of the items.
    public IEnumerable<string> ItemValues { get { return Items.Select(i => (string)i); } }

    [XmlAttribute(AttributeName = "id")]
    public string Id { get; set; }
}

Working .Net fiddle.

For more examples of using [XmlChoiceIdentifier] to deserialize a sequence of elements with different names to c# objects of the same type, see for instance here or here.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • Thanks for the advice with multiple XmlElement Attributes on the same property. I can now just add the ElementName in the Attribute to map to custom classes. – Lumo Dec 12 '19 at 13:08
-1

No, basically; XmlSerializer doesn't support that. If you want to use that option you'd need to write it manually with XDocument or XmlDocument or XmlReader.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900