0

I have class that implement list of custom class. That class also has two properties. But when I serialize that class, XML contains only array of my custom classes but don't contains another two properties. Here is the class:

public class Porudzbina : List<PorudzbenicaStavka>, IEnumerable<SqlDataRecord>
{
    public long KomSifra { get; set; }
    public Guid KomId { get; set; }

    IEnumerator<SqlDataRecord> IEnumerable<SqlDataRecord>.GetEnumerator()
    {
        var sqlRow = new SqlDataRecord(
              new SqlMetaData("rb", SqlDbType.Int),
              new SqlMetaData("RobaSifra", SqlDbType.NVarChar, 50),
              new SqlMetaData("RobaNaziv", SqlDbType.NVarChar, 100)
             );
        foreach (PorudzbenicaStavka por in this)
        {
            sqlRow.SetInt32(0, por.rb);
            sqlRow.SetString(1, por.RobaSifra);
            sqlRow.SetString(2, por.RobaNaziv);
            yield return sqlRow;
        }
    }
}

and code that I use to serialize it:

    XmlSerializer serializer = new XmlSerializer(typeof(Porudzbina));
    using (TextWriter writer = new StreamWriter(@"C:\Xmle.xml"))
    {
        serializer.Serialize(writer, por);
    } 

and this is XML that I got:

    <?xml version="1.0" encoding="utf-8"?>
<ArrayOfPorudzbenicaStavka xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <PorudzbenicaStavka>
    <rb>1</rb>
    <RobaSifra>3702</RobaSifra>
    <RobaNaziv>Foullon mlecna cokolada 33% Ecuador 100g</RobaNaziv>    
  </PorudzbenicaStavka>
  <PorudzbenicaStavka>
    <rb>2</rb>
    <RobaSifra>1182</RobaSifra>
    <RobaNaziv>IL Capitano zelena maslina sa paprikom 720g</RobaNaziv>    
  </PorudzbenicaStavka>
  <PorudzbenicaStavka>
    <rb>3</rb>
    <RobaSifra>1120</RobaSifra>
    <RobaNaziv>Kaiser tuna steak sa papricicom u ulju 170g.</RobaNaziv>    
  </PorudzbenicaStavka>
</ArrayOfPorudzbenicaStavka>

I want my xml contains two properties together with a array of custom class, that I could deserialize it into its original state...

dbc
  • 104,963
  • 20
  • 228
  • 340
Naum
  • 125
  • 1
  • 3
  • 12

2 Answers2

1

The reason that your properties are not deserialized is explained in the documentation section Serializing a Class that Implements the ICollection Interface:

You can create your own collection classes by implementing the ICollection interface, and use the XmlSerializer to serialize instances of these classes. Note that when a class implements the ICollection interface, only the collection contained by the class is serialized. Any public properties or fields added to the class will not be serialized.

So, that's that.

You might consider changing your design so your classes do not have properties. For some reasons to make this change, see Why not inherit from List?.

If you nevertheless choose to have a collection with serializable properties, you're going to need to manually implement IXmlSerializable. This is burdensome, since you need to handle many "edge" cases including empty elements, unexpected elements, comments, and presence or absence of whitespace, all of which can throw off your ReadXml() method. For some background, see How to Implement IXmlSerializable Correctly.

First, create a base class for generic lists with serializable properties:

public class XmlSerializableList<T> : List<T>, IXmlSerializable where T : new()
{
    public XmlSerializableList() : base() { }

    public XmlSerializableList(IEnumerable<T> collection) : base(collection) { }

    public XmlSerializableList(int capacity) : base(capacity) { }

    #region IXmlSerializable Members

    const string CollectionItemsName = "Items";
    const string CollectionPropertiesName = "Properties";

    void IXmlSerializable.WriteXml(XmlWriter writer)
    {
        // Do not write the wrapper element.

        // Serialize the collection.
        WriteCollectionElements(writer);

        // Serialize custom properties.
        writer.WriteStartElement(CollectionPropertiesName);
        WriteCustomElements(writer);
        writer.WriteEndElement();

        // Do not end the wrapper element.
    }

    private void WriteCollectionElements(XmlWriter writer)
    {
        if (Count < 1)
            return;
        // Serialize the collection.
        writer.WriteStartElement(CollectionItemsName);

        var serializer = new XmlSerializer(typeof(T));
        var ns = new XmlSerializerNamespaces();
        ns.Add("", ""); // Disable the xmlns:xsi and xmlns:xsd lines.
        foreach (var item in this)
        {
            serializer.Serialize(writer, item, ns);
        }

        writer.WriteEndElement();
    }

    /// <summary>
    /// Write ALL custom elements to the XmlReader
    /// </summary>
    /// <param name="writer"></param>
    protected virtual void WriteCustomElements(XmlWriter writer)
    {
    }

    void IXmlSerializable.ReadXml(XmlReader reader)
    {
        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)
                // Comment, whitespace
                reader.Read();
            else if (reader.IsEmptyElement)
                reader.Read();
            else if (reader.Name == CollectionItemsName)
                ReadCollectionElements(reader);
            else if (reader.Name == CollectionPropertiesName)
                ReadCustomElements(reader);
            else
                // Unknown element, skip it.
                reader.Skip();
        }

        // Move past the end of the wrapper element
        reader.ReadEndElement();
    }

    void ReadCustomElements(XmlReader reader)
    {
        reader.ReadStartElement(); // Advance to the first sub element of the collection element.
        while (reader.NodeType != XmlNodeType.EndElement)
        {
            if (reader.NodeType == XmlNodeType.Element)
            {
                using (var subReader = reader.ReadSubtree())
                {
                    while (subReader.NodeType != XmlNodeType.Element) // Read past XmlNodeType.None
                        if (!subReader.Read())
                            break;
                    ReadCustomElement(subReader);
                }
            }
            reader.Read();
        }
        // Move past the end of the properties element
        reader.Read();
    }

    void ReadCollectionElements(XmlReader reader)
    {
        var serializer = new XmlSerializer(typeof(T));
        reader.ReadStartElement(); // Advance to the first sub element of the collection element.
        while (reader.NodeType != XmlNodeType.EndElement)
        {
            if (reader.NodeType == XmlNodeType.Element)
            {
                using (var subReader = reader.ReadSubtree())
                {
                    while (subReader.NodeType != XmlNodeType.Element) // Read past XmlNodeType.None
                        if (!subReader.Read())
                            break;
                    var item = (T)serializer.Deserialize(subReader);
                    Add(item);
                }
            }
            reader.Read();
        }
        // Move past the end of the collection element
        reader.Read();
    }

    /// <summary>
    /// Read ONE custom element from the XmlReader
    /// </summary>
    /// <param name="reader"></param>
    protected virtual void ReadCustomElement(XmlReader reader)
    {
    }

    XmlSchema IXmlSerializable.GetSchema()
    {
        return null;
    }

    #endregion
}

To use this class, you will need to override ReadCustomElement(XmlReader reader), which reads a single custom property, and WriteCustomElements(XmlWriter writer), which writes all custom properties. (Note the asymmetry, it makes implementing class hierarchies a little easier.) Then create your Porudzbina class as follows:

public class Porudzbina : XmlSerializableList<PorudzbenicaStavka>
{
    public long KomSifra { get; set; }
    public Guid KomId { get; set; }

    const string KomSifraName = "KomSifra";
    const string KomIdName = "KomId";

    protected override void WriteCustomElements(XmlWriter writer)
    {
        writer.WriteElementString(KomSifraName, XmlConvert.ToString(KomSifra));
        writer.WriteElementString(KomIdName, XmlConvert.ToString(KomId));
        base.WriteCustomElements(writer);
    }

    protected override void ReadCustomElement(XmlReader reader)
    {
        if (reader.Name == KomSifraName)
        {
            KomSifra = reader.ReadElementContentAsLong();
        }
        else if (reader.Name == KomIdName)
        {
            var s = reader.ReadElementContentAsString();
            KomId = XmlConvert.ToGuid(s);
        }
        else
        {
            base.ReadCustomElement(reader);
        }
    }
}

This will create XML that looks like:

<Porudzbina>
    <Items>
        <PorudzbenicaStavka>
            <!-- contents of first PorudzbenicaStavka -->
        </PorudzbenicaStavka>
        <!-- Additional PorudzbenicaStavka -->
    </Items>
    <Properties>
        <KomSifra>101</KomSifra>
        <KomId>bb23a3b8-23d3-4edd-848b-d7621e6ed2c0</KomId>
    </Properties>
</Porudzbina>
dbc
  • 104,963
  • 20
  • 228
  • 340
0

I've uploaded my serialization library to Github, where such issues are handled.

Atlas Xml Serializer

I'm assuming that you have below data classes. I've just added [XmlElement] attribute over properties to force them serialize into xml elements.

public class Porudzbina : List<PorudzbenicaStavka>, IEnumerable<SqlDataRecord>
{
    [XmlElement]
    public long KomSifra { get; set; }

    [XmlElement]
    public Guid KomId { get; set; }

    IEnumerator<SqlDataRecord> IEnumerable<SqlDataRecord>.GetEnumerator()
    {
        var sqlRow = new SqlDataRecord(
              new SqlMetaData("rb", SqlDbType.Int),
              new SqlMetaData("RobaSifra", SqlDbType.NVarChar, 50),
              new SqlMetaData("RobaNaziv", SqlDbType.NVarChar, 100)
             );
        foreach (PorudzbenicaStavka por in this)
        {
            sqlRow.SetInt32(0, por.rb);
            sqlRow.SetString(1, por.RobaSifra);
            sqlRow.SetString(2, por.RobaNaziv);
            yield return sqlRow;
        }
    }
}

public class PorudzbenicaStavka
{
    [XmlElement]
    public int rb { get; set; }

    [XmlElement]
    public string RobaSifra { get; set; }

    [XmlElement]
    public string RobaNaziv { get; set;  }
}

And here's the instance:

var o = new Porudzbina
{
    new PorudzbenicaStavka { rb=1, RobaSifra="3702", RobaNaziv="Foullon mlecna cokolada 33% Ecuador 100g" },
    new PorudzbenicaStavka { rb=2, RobaSifra="1182", RobaNaziv="IL Capitano zelena maslina sa paprikom 720g" },
    new PorudzbenicaStavka { rb=3, RobaSifra="1120", RobaNaziv="Kaiser tuna steak sa papricicom u ulju 170g." },
};

o.KomId = new Guid("{EC63AEC3-1512-451F-B967-836DD0E9820A}");
o.KomSifra = 999999;

And here's how atlas xml serialization library does the job:

var serialized = Atlas.Xml.Serializer.Serialize(o, true);
var deserialized = Atlas.Xml.Serializer.Deserialize<Porudzbina>(serialized);

Xml would look like this:

<Porudzbina>
  <KomSifra>999999</KomSifra>
  <KomId>ec63aec3-1512-451f-b967-836dd0e9820a</KomId>
  <item>
    <rb>1</rb>
    <RobaSifra>3702</RobaSifra>
    <RobaNaziv>Foullon mlecna cokolada 33% Ecuador 100g</RobaNaziv>
  </item>
  <item>
    <rb>2</rb>
    <RobaSifra>1182</RobaSifra>
    <RobaNaziv>IL Capitano zelena maslina sa paprikom 720g</RobaNaziv>
  </item>
  <item>
    <rb>3</rb>
    <RobaSifra>1120</RobaSifra>
    <RobaNaziv>Kaiser tuna steak sa papricicom u ulju 170g.</RobaNaziv>
  </item>
</Porudzbina>

If you change the data class like this:

[Atlas.Xml.XmlSerializationType(ChildElementName = "PorudzbenicaStavka")]
public class Porudzbina : List<PorudzbenicaStavka>, IEnumerable<SqlDataRecord>
{
    public long KomSifra { get; set; }
    public Guid KomId { get; set; }
    // ...
}

public class PorudzbenicaStavka
{
    public int rb { get; set; }
    public string RobaSifra { get; set; }

    [XmlText]
    public string RobaNaziv { get; set;  }
}

Then, serialized class would be like this:

<Porudzbina KomSifra="999999" KomId="ec63aec3-1512-451f-b967-836dd0e9820a">
  <PorudzbenicaStavka rb="1" RobaSifra="3702">Foullon mlecna cokolada 33% Ecuador 100g</PorudzbenicaStavka>
  <PorudzbenicaStavka rb="2" RobaSifra="1182">IL Capitano zelena maslina sa paprikom 720g</PorudzbenicaStavka>
  <PorudzbenicaStavka rb="3" RobaSifra="1120">Kaiser tuna steak sa papricicom u ulju 170g.</PorudzbenicaStavka>
</Porudzbina>