Click here to Skip to main content
15,861,125 members
Articles / Programming Languages / XML
Article

Enumerate over XML data in a foreach loop

Rate me:
Please Sign up or sign in to vote.
4.91/5 (22 votes)
11 Jan 20053 min read 98.8K   892   50   9
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:

XML
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.

C#
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.

C#
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.

C#
// 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


Written By
Software Developer (Senior)
United States United States
Josh creates software, for iOS and Windows.

He works at Black Pixel as a Senior Developer.

Read his iOS Programming for .NET Developers[^] book to learn how to write iPhone and iPad apps by leveraging your existing .NET skills.

Use his Master WPF[^] app on your iPhone to sharpen your WPF skills on the go.

Check out his Advanced MVVM[^] book.

Visit his WPF blog[^] or stop by his iOS blog[^].

See his website Josh Smith Digital[^].

Comments and Discussions

 
QuestionHow do you get the customer ID into the customer class ? Pin
Michael Abramovitch7-Sep-07 8:19
Michael Abramovitch7-Sep-07 8:19 
AnswerRe: How do you get the customer ID into the customer class ? Pin
Josh Smith7-Sep-07 8:27
Josh Smith7-Sep-07 8:27 
GeneralUnable to convert to VS 2005 Pin
Stan Tarver18-Oct-06 3:08
Stan Tarver18-Oct-06 3:08 
GeneralNice work!!! Pin
Rajesh Pillai12-Jan-05 1:01
Rajesh Pillai12-Jan-05 1:01 
GeneralRe: Nice work!!! Pin
Josh Smith12-Jan-05 2:25
Josh Smith12-Jan-05 2:25 
GeneralJust a note Pin
mostyn24-Nov-04 6:40
mostyn24-Nov-04 6:40 
GeneralRe: Just a note Pin
Josh Smith9-Jan-05 7:07
Josh Smith9-Jan-05 7:07 
GeneralRe: Just a note Pin
27-Apr-05 16:00
suss27-Apr-05 16:00 
GeneralRe: Just a note Pin
Josh Smith27-Apr-05 16:55
Josh Smith27-Apr-05 16:55 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.