'XmlReader stops deserializing after first array property

I have a custom list used to have features of BindingList<T> while also being XML serializable. The SBindingList<T> class is as follows:

public class SBindingList<T> : BindingList<T>, IXmlSerializable
{
    public XmlSchema GetSchema()
    {
        return null;
    }

    public void ReadXml(XmlReader reader)
    {
        reader.Read();
        XmlSerializer serializer = new XmlSerializer(typeof(List<T>));
        List<T> ts = (List<T>)serializer.Deserialize(reader);
        foreach (T item in ts)
        {
            Add(item);
        }
    }

    public void WriteXml(XmlWriter writer)
    {
        XmlSerializer serializer = new XmlSerializer(typeof(List<T>));
        serializer.Serialize(writer, this.ToList());
    }
}

The idea is to read and write the XML as if it was a List<T> behind the scenes but still works and acts like a BindingList<T>. The issue I'm having is that it starts reading <Games> then when it gets to Add(item); starts to deserialize <AvailableQuests>. Because <AvailableQuests> is empty it just falls out of ReadXml but goes immediately back finishing up adding <Games> and just falls out of ReadXml without even touching <Characters> or <ActiveQuests>. The <Games>, <AvailableQuests>, <Characters>, and <ActiveQuests> are all SBindingList<T>s. Also note that I redacted all the IDs for privacy reasons. Just replace the [blah] tags with any ulong.

<Games>
    <ArrayOfGame xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:xsd="http://www.w3.org/2001/XMLSchema">
        <Game GuildID="[GuildID]" BotChannel="0" QuestChannel="0">
            <AvailableQuests>
                <ArrayOfQuest />
            </AvailableQuests>
            <Characters>
                <ArrayOfCharacter>
                    <Character Name="The Real Dirty Dan" Class="Meme" Level="17"
                        OwnerID="[Owner1]" Busy="false" />
                    <Character Name="Bob" Class="Builder" Level="2" OwnerID="[Owner2]"
                        Busy="false" />
                </ArrayOfCharacter>
            </Characters>
            <ActiveQuests>
                <ArrayOfQuest />
            </ActiveQuests>
        </Game>
    </ArrayOfGame>
</Games>

Here is the object setup incase someone needs it:

public class Game
{
    /// <summary>
    /// Only for serialization
    /// </summary>
    public Game() { }

    public Game(ulong guildID)
    {
        GuildID = guildID;
    }

    /// <summary>
    /// What discord server is the game on
    /// </summary>
    [XmlAttribute]
    public ulong GuildID { get; set; }

    [XmlAttribute]
    public ulong BotChannel { get; set; }

    [XmlAttribute]
    public ulong QuestChannel { get; set; }

    public SBindingList<Quest> AvailableQuests { get; set; } = new SBindingList<Quest>();

    public SBindingList<Character> Characters { get; set; } = new SBindingList<Character>();

    public SBindingList<Quest> ActiveQuests { get; set; } = new SBindingList<Quest>();

    public string Test { get; set; } // This gets ignored too

    public Character GetCharacterByName(string name)
    {
        return (from c in Characters
                where c.Name == name
                select c).FirstOrDefault();
    }
}

I don't really know where to start with this one. I've tried using a List<T> or just reading each T one at a time but both ways end up ignoring all other elements. My only guess is that I need to clean something up with the reader before I can let it fall out of ReadXml like reader.FinishedReading().



Solution 1:[1]

The answer to this lies in the documentation for ReadXml:

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.

You seem to have spotted this by calling reader.Read() at the start (to move past the start tag and onto the content), but you haven't at the end. The fix is to call reader.Read() again afterwards.

Perhaps a better approach would be to call reader.ReadStartElement() first and reader.ReadEndElement() at the end. This just calls reader.Read() internally, but it will throw an exception in the case the reader isn't in the expected state.

I'd also suggest sharing a static serialiser instance rather than creating a new one each time you read or write a list. So this should work:

public class SBindingList<T> : BindingList<T>, IXmlSerializable
{
    private static readonly XmlSerializer Serializer = new XmlSerializer(typeof(List<T>));
    
    public XmlSchema GetSchema()
    {
        return null;
    }

    public void ReadXml(XmlReader reader)
    {
        reader.ReadStartElement();

        var items = (List<T>)Serializer.Deserialize(reader);
        
        foreach (T item in items)
        {
            Add(item);
        }

        reader.ReadEndElement();
    }

    public void WriteXml(XmlWriter writer)
    {
        Serializer.Serialize(writer, this.ToList());
    }
}

Solution 2:[2]

If reader is on any other element besides the next element, the reader gives up deserizing the rest of the object. In other words, the reader must be on the next element (In this case the Character element) to continue to deserialize the <Game> tag. In this case it can be fixed by using reader.Skip() then reader.ReadEndElement()

Solution 3:[3]

The reader needs to be at the next element (in this case <Characters>) in the original object otherwise it just returns if it is at a end element node. This solution isn't failproof or the cleanest but it gets the job done. If I find something better that confirms that it is at the next elemet before falling out I will edit this answer.

public void ReadXml(XmlReader reader)
{
    reader.Read();
    XmlSerializer serializer = new XmlSerializer(typeof(List<T>));
    XmlReader xmlReader = reader.ReadSubtree();
    xmlReader.Read();
    List<T> ts = (List<T>)serializer.Deserialize(xmlReader);
    foreach (T item in ts)
    {
        Add(item);
    }
    xmlReader.Close();
    reader.Skip();
    reader.ReadEndElement();
}

The key is reader.Skip() and reader.ReadEndElement() lines. The Skip() function moves the reader to </AvailableQuests> and the ReadEndElement() makes sure that it is an end element (Like it needs to be) and moves to <Characters>.

Edit: use https://stackoverflow.com/a/71969803/4771954

Solution 4:[4]

Try following :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Serialization;

namespace ConsoleApplication23
{
    class Program
    {
        const string FILENAME = @"c:\temp\test.xml";
        static void Main(string[] args)
        {
            XmlReader reader = XmlReader.Create(FILENAME);
            XmlSerializer serializer = new XmlSerializer(typeof(Games));
            Games games = (Games)serializer.Deserialize(reader);
        }
    }
    public class Games
    {
        [XmlArray(ElementName = "ArrayOfGame")]
        [XmlArrayItem(ElementName = "Game")]
        public List<Game> game { get; set; }
 
    }
    public class Game
    {
        [XmlAttribute()]
        public string GuildID { get; set; }
        [XmlAttribute()]
        public int BotChannel { get; set; }
        [XmlAttribute()]
        public int QuestChannel { get; set; }
        [XmlArray(ElementName = "AvailableQuests")]
        [XmlArrayItem(ElementName = "ArrayOfQuest")]
        public List<Quest> availableQuest { get; set; }
        [XmlElement(ElementName = "Characters")]
        public Characters characters { get; set; }
        [XmlArray(ElementName = "ActiveQuests")]
        [XmlArrayItem(ElementName = "ArrayOfQuest")]
        public List<Quest> activeQuest { get; set; }
        public class Quest
    {
    }
    public class Characters
    {
        [XmlArray(ElementName = "ArrayOfCharacter")]
        [XmlArrayItem(ElementName = "Character")]
        public List<Character> character { get; set; }
    }
    public class Character
    {
        [XmlAttribute()]
        public string Name { get; set; }
        [XmlAttribute()]
        public string Class { get; set; }
        [XmlAttribute()]
        public int Level { get; set; }
        [XmlAttribute()]
        public string OwnerID { get; set; }
        [XmlAttribute()]
        public Boolean Busy{ get; set; }
    }
    public class ArrayOfQuest
    {
    }

}}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Charles Mager
Solution 2 Doshorte Dovencio
Solution 3
Solution 4 jdweng