6

I'm writing a file reader using the XmlReader in a Silverlight project. However, I'm getting some errors (specifically around the XmlReader.ReadStartElement method) and it's causing me to believe that I've misunderstood how to use it somewhere along the way.

Basically, here is a sample of the format of the Xml I am using:

<?xml version="1.0" encoding="utf-8" standalone="no"?>
<root>
    <EmptyElement />
    <NonEmptyElement Name="NonEmptyElement">
        <SubElement Name="SubElement" />
    </NonEmptyElement>
</root>

And here is a sample of some code used in the same way as how I am using it:

public void ReadData(XmlReader reader)
{
    // Move to root element
    reader.ReadStartElement("root");

    // Move to the empty element
    reader.ReadStartElement("EmptyElement");

    // Read any children
    while(reader.ReadToNextSibling("SubEmptyElement"))
    {
        // ...
    }

    // Read the end of the empty element
    reader.ReadEndElement();

    // Move to the non empty element
    reader.ReadStartElement("NonEmptyElement");    // NOTE: This is where I get the error.

    // ...
}

So, essentially, I am simply trying to read each element and any contained children. The error I get at the highlighted point is as follows:

Error Description

[Xml_InvalidNodeType] Arguments: None,10,8 Debugging resource strings are unavailable. Often the key and arguments provide sufficient information to diagnose the problem. See http://go.microsoft.com/fwlink/?linkid=106663&Version=4.0.51204.0&File=System.Xml.dll&Key=Xml_InvalidNodeType

Error Stack Trace

at System.Xml.XmlReader.ReadStartElement(String name) at ----------------

Any advice or direction on this would be greatly appreciated.

EDIT Since this reader needs to be fairly generic, it can be assumed that the Xml may contain elements that are children of the EmptyElement. As such, the attempt at reading any SubEmptyElements should be valid.

Samuel Slade
  • 8,405
  • 6
  • 33
  • 55

1 Answers1

7

<SubElement/> is not a sibling of <EmptyElement>, so <NonEmptyElement> is going to get skipped entirely, and your call to ReadEndElement() will read the end element </root>. When you try to subsequently read "NonEmptyElement", there are no elements left, and you'll get an XmlException: {"'None' is an invalid XmlNodeType. Line 8, position 1."}

Note also that since <EmptyElement/> is empty, when you ReadStartElement("EmptyElement"), you'll read the whole element, and you won't need to use ReadEndElement().

I'd also recommend that you configure your reader settings to IgnoreWhitespace (if you're not already doing so), to avoid any complications introduced by reading (insignificant) whitespace text nodes when you aren't expecting them.

Try moving the Read of NonEmptyElement up:

public static void ReadData(XmlReader reader)
{
    reader.ReadStartElement("root");

    reader.ReadStartElement("EmptyElement");

    reader.ReadStartElement("NonEmptyElement");

    while (reader.ReadToNextSibling("SubEmptyElement"))
    {
        // ...
    }

    reader.ReadEndElement(/* NonEmptyElement */);

    reader.ReadEndElement(/* root */);
    // ...
}

If you just want to skip anything in <EmptyElement>, regardless of whether or not its actually empty, use ReadToFollowing:

public static void ReadData(XmlReader reader)
{
    reader.ReadStartElement("root");

    reader.ReadToFollowing("NonEmptyElement");

    Console.WriteLine(reader.GetAttribute("Name"));

    reader.ReadStartElement("NonEmptyElement");

    Console.WriteLine(reader.GetAttribute("Name"));
    while (reader.ReadToNextSibling("SubEmptyElement"))
    {
        // ...
    }

    reader.ReadEndElement(/* NonEmptyElement */);

    reader.ReadEndElement(/* root */);
    // ...
}

Update: Here's a fuller example with a clearer data model. Maybe this is closer to what you're asking for.

XMLFile1.xml:

<?xml version="1.0" encoding="utf-8" standalone="no"?>
<root>
  <Person Type="Homeless"/>
  <Person Type="Developer">
    <Home Type="Apartment" />
  </Person>
  <Person Type="Banker">
    <Home Type="Apartment"/>
    <Home Type="Detached"/>
    <Home Type="Mansion">
      <PoolHouse/>
    </Home>
  </Person>
</root>

Program.cs:

using System;
using System.Xml;

namespace ConsoleApplication6
{
    internal class Program
    {
        public static void ReadData(XmlReader reader)
        {
            reader.ReadStartElement("root");

            while (reader.IsStartElement("Person"))
            {
                ReadPerson(reader);
            }

            reader.ReadEndElement( /* root */);
        }

        public static void ReadPerson(XmlReader reader)
        {
            Console.WriteLine(reader.GetAttribute("Type"));
            bool isEmpty = reader.IsEmptyElement;
            reader.ReadStartElement("Person");
            while (reader.IsStartElement("Home"))
            {
                ReadHome(reader);
            }
            if (!isEmpty)
            {
                reader.ReadEndElement( /* Person */);
            }
        }

        public static void ReadHome(XmlReader reader)
        {
            Console.WriteLine("\t" + reader.GetAttribute("Type"));
            bool isEmpty = reader.IsEmptyElement;
            reader.ReadStartElement("Home");

            if (!isEmpty)
            {
                reader.Skip();
                reader.ReadEndElement( /* Home */);
            }
        }

        private static void Main(string[] args)
        {
            var settings = new XmlReaderSettings { IgnoreWhitespace = true };
            using (var xr = XmlReader.Create("XMLFile1.xml", settings))
            {
                ReadData(xr);
            }
            Console.ReadKey();
        }
    }
}
lesscode
  • 6,221
  • 30
  • 58
  • Ok, that makes sense. However, in the Xml, it is valid to have child elements within EmptyElement (I probably should have explained this in the question). As such, I need some way of determining whether to call the ReadToNextSibling and ReadEndElement after the call to ReadStartElement("EmptyElement"). I have tried using IsEmptyElement, but that only returns false. Any ideas? – Samuel Slade Feb 13 '11 at 11:58
  • Are you saying you want to skip EmptyElement, regardless of whether or not it contains children? See the updated answer. – lesscode Feb 13 '11 at 14:13
  • No no, the idea is to actually read that element. But it is possible for that element to contain children or no children. For example, say you have a database of people, and that EmptyElement is actually Children - referring to the children a person may or may not have. If that person has no children, the element will be empty. However, if they do have children, there will be elements within that element. So, my questions effectively is; how can you read that element and if it has children, read those, if not, skip to the next element (which could be Parents, or something)? – Samuel Slade Feb 13 '11 at 14:29
  • I think it would help if you posted an example that's closer to your real object model. – lesscode Feb 13 '11 at 14:44
  • I guessed at something closer to your real schema - see updated answer. – lesscode Feb 13 '11 at 15:04
  • Ah-hah! The combined use of reader.IsStartElement and reader.IsEmptyElement solved it! I tried using the IsEmptyElement after your original answer, but it kept returning false; but that extra check seems to have sorted it now. Many thanks! – Samuel Slade Feb 15 '11 at 12:25
  • You're welcome. You may have been checking IsEmptyElement _after_ already calling ReadStartElement. In this case, the whole (empty) element has already been read in its entirety and you're checking the _next_ element. – lesscode Feb 15 '11 at 14:32
  • @lesscode Hi, I am confused with `ReadStartElement()`, say we have a XmlReader `r` to read the simplest xml: `Joe`, firstly `r.ReadStartElement("firstname");` then why `Console.WriteLine(r.Value)` prints `Joe` even before calling `r.Read()` to read the `Text` element? I thought what `ReadStartElement(string)` does is verifies it's currently on a start element with the given element name then reads this element. Why does it somehow read the next node as well? – dragonfly02 Feb 07 '17 at 21:20
  • @stt106: After ReadStartElement(), the reader is positioned on the next node (the Text node): https://msdn.microsoft.com/en-us/library/y7e4769a(v=vs.110).aspx, and XmlReader.Value returns the text value of the node where the reader is positioned: https://msdn.microsoft.com/en-us/library/system.xml.xmlreader.value(v=vs.110).aspx – lesscode Feb 08 '17 at 02:20
  • Ah ok thanks. I thought to get a value of a node it has to be read first but apparently it's the the other way round. – dragonfly02 Feb 08 '17 at 07:50