Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Enumerate over XML data in a foreach loop

0.00/5 (No votes)
11 Jan 2005 1  
An article which shows how to enumerate over XML data in a foreach loop as if the data were in a collection.

Introduction

This article demonstrates how to use a foreach loop to iterate over data in an XML file as if the data were stored in a collection of objects. The fundamental concept presented here is that .NET's IEnumerable/IEnumerator mechanism does not necessarily have to be used in conjunction with collection classes.

Background

Suppose that you have an XML file containing some data that you want to show to the user. There are a variety of ways to get that data from the XML file onto the output device. Many of these techniques involve reading the entire file into some data structure and then showing some subset of the cached data. Wouldn't it be nice if you could just "iterate over the XML file" itself and only store the values that you want to show? Wouldn't it be even nicer if you could perform this iteration with a simple foreach loop? Fortunately, the .NET Framework allows for this type of flexibility and elegance via the IEnumerable and IEnumerator interfaces.

Before we look at the code, here is a refresher on how the foreach loop operates. The object which is being iterated over must implement the IEnumerable interface. IEnumerable has one method, GetEnumerator(), which returns an object that implements the IEnumerator interface. IEnumerator has three public members:

  • A property called Current which returns the "current" item in the enumerated list of values.
  • A method called MoveNext() which advances the enumerator to the "next" item in the list of values and returns false if there are no more items to iterate over.
  • A method called Reset() which positions the enumerator "before" the first item in the list of values.

Prior to the foreach mechanism requesting the current item via the Current property, it will invoke the MoveNext() method. Assuming that MoveNext() returns true, Current is invoked and its return value becomes the "local temp" variable within the scope of the foreach loop.

Using the code

The sample project accompanying this article is quite simple. Its purpose is to demonstrate the technique being presented, but it could easily be extended to fit more sophisticated needs. The sample contains an XML file containing customer information, with a very simple schema:

CUSTOMERS.XML

<?xml version="1.0" encoding="utf-8" ?> 
<Customers>
 <Customer id="1">
 <FirstName>Abe</FirstName>
 <LastName>Lalice</LastName>
 <Orders>7</Orders>
 <Balance>340.95</Balance>
 </Customer>
 <Customer id="2">
 <FirstName>Mary</FirstName>
 <LastName>Poolsworth</LastName>
 <Orders>14</Orders>
 <Balance>3782.02</Balance>
 </Customer>
 <Customer id="3">
 <FirstName>Perth</FirstName>
 <LastName>Higgins</LastName>
 <Orders>1</Orders>
 <Balance>42.00</Balance>
 </Customer>
 <Customer id="4">
 <FirstName>David</FirstName>
 <LastName>Applegate</LastName>
 <Orders>2</Orders>
 <Balance>232.50</Balance>
 </Customer>
 <Customer id="5">
 <FirstName>Martha</FirstName>
 <LastName>Whithersby</LastName>
 <Orders>26</Orders>
 <Balance>19023.07</Balance>
 </Customer>
</Customers>

Below is the code that takes certain customers from the XML file and puts them into a ListBox control. The beauty of this is that from the client's perspective (the user of the Customers class), there is no way of knowing that the data is being read in from an XML file on-the-fly. This implementation fact is completely encapsulated.

private void CustomerEnumerationForm_Load(object sender,
  System.EventArgs e)
{
 //

 // Here we create a Customers object and implicitly

 // use the CustomerEnumerator to iterate

 // over all of the customers in the XML file. 

 // From our perspective here, it seems

 // as though the Customers class has a collection

 // containing all of the customers.  In

 // reality, though, the CustomerEnumerator is reading

 // in each customer from Customers.xml 

 // one at a time, as they are requested by the foreach loop.

 //

 //

 // Note, Customers implements IDisposable.

 // This is necessary in case the foreach loop is

 // exited before iterating over all of the customers

 // in the file. You do not have to explicitly

 // invoke the Dispose method, however, because a finalizer

 // is in place to handle disposing of the

 // XMLTextReader if the client code does not.

 using( Customers customers = new Customers() )
 {
    foreach( Customer cust in customers )
        if( cust.Orders > 2 )
            this.lstCustomers.Items.Add( cust );
 }
}

The Customers class represents all of the customers in the file.

public class Customers : IEnumerable
{
 // When the foreach loop begins, this method is invoked 

 // so that the loop gets an enumerator to query.

 public IEnumerator GetEnumerator()
 {
  return new CustomerEnumerator();
 }
}

The CustomerEnumerator class contains the code that reads in the customer data from the XML file.

// Exposes the customer data found in Customers.xml.

public class CustomerEnumerator : IEnumerator
{
 // Data

 private readonly string fileName = @"..\..\Customers.xml";
 private XmlTextReader reader;


 // IEnumerator Members

 
 // Called when the enumerator needs to be reinitialized.

 public void Reset()
 {
  if( this.reader != null )
   this.reader.Close();
  System.Diagnostics.Debug.Assert( File.Exists( this.fileName ), 
   "Customer file does not exist!" );
  StreamReader stream = new StreamReader( this.fileName );
  this.reader         = new XmlTextReader( stream );
 }

 // Called just prior to Current being invoked.  

 // If true is returned then the foreach loop will

 // try to get another value from Current.  

 // If false is returned then the foreach loop terminates.

 public bool MoveNext()
 {
  // Call Reset the first time MoveNext is called 

  // instead of in the constructor 

  // so that we keep the stream open only as long as needed.

  if( this.reader == null )
   this.Reset();
   
  if( this.FindNextTextElement() )
   return true;
   
  // If there are no more text elements in the XML file then

  // we have read in all of the data 

  // and the foreach loop should end.

  this.reader.Close();
  return false;
 }

 // Invoked every time MoveNext() returns true. 

 // This extracts the values for the "current" customer from 

 // the XML file and returns that data packaged up as a Customer object.

 public object Current
 {
  get
  {
    // No need to call FindNextTextElement here

    // because it was called for us by MoveNext().

    string firstName = this.reader.Value;

    // Set 'val' to a default value in case the element was empty.

    string val = "";
    if( this.FindNextTextElement() )
    val = this.reader.Value;

    string lastName = val;

    // Set 'val' to a default value in case the element was empty.

    val = "0";
    if( this.FindNextTextElement() )
    val = this.reader.Value;

    int     orders;
    try   { orders = Int32.Parse( val ); }
    catch { orders = Int32.MinValue;     }

    // Set 'val' to a default value in case the element was empty.

    val = "0";
    if( this.FindNextTextElement() )
    val = this.reader.Value;

    decimal balance;
    try   { balance = Decimal.Parse( val ); }
    catch { balance = Decimal.MinValue;     }

    return new Customer( firstName, lastName, orders, balance );
  }
 }


 // Private Helper


 // Advances the XmlTextReader to the next Text

 // element in the XML stream.

 // Returns true if there is more data to be read

 // from the stream, else false.

 private bool FindNextTextElement()
 {
    bool readOn = this.reader.Read();
    bool prevTagWasElement = false;
    while( readOn && this.reader.NodeType != XmlNodeType.Text )
    {
        // If the current element is empty, stop reading and return false.

        if( prevTagWasElement &&  this.reader.NodeType == XmlNodeType.EndElement )
            readOn = false;
        prevTagWasElement = this.reader.NodeType == XmlNodeType.Element;
        readOn = readOn && this.reader.Read();
    }
    return readOn;
 }
}

There is a very dumb Customer class that just holds onto the data extracted from the XML file so that the enumerator has a way to bundle up all of the data concerning a single customer. I won't bother showing it here because there is nothing in the class that is relevant to the topic of this article. It is available in the sample project, though.

Conclusion

While this novel idea might not be the next silver bullet in software development, I hope that you find it helpful or at least interesting. I know that I did, or else I wouldn't have bothered writing an article about it! :)

Updates

  • 1/9/05 - Fixed FindNextTextElement() to handle situations where element does not contain a TextElement. Also implemented IDisposable on Customers and CustomerEnumerator as well as added a finalizer to CustomerEnumerator. All of those methods were added for the case where the user of the CustomerEnumerator breaks from the foreach loop before iterating over all the elements in the file. There needed to be a way to close the XmlTextReader.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here