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
="1.0" ="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)
{
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
{
public IEnumerator GetEnumerator()
{
return new CustomerEnumerator();
}
}
The CustomerEnumerator class contains the code that reads in the customer data from the XML file.
public class CustomerEnumerator : IEnumerator
{
private readonly string fileName = @"..\..\Customers.xml";
private XmlTextReader reader;
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 );
}
public bool MoveNext()
{
if( this.reader == null )
this.Reset();
if( this.FindNextTextElement() )
return true;
this.reader.Close();
return false;
}
public object Current
{
get
{
string firstName = this.reader.Value;
string val = "";
if( this.FindNextTextElement() )
val = this.reader.Value;
string lastName = val;
val = "0";
if( this.FindNextTextElement() )
val = this.reader.Value;
int orders;
try { orders = Int32.Parse( val ); }
catch { orders = Int32.MinValue; }
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 bool FindNextTextElement()
{
bool readOn = this.reader.Read();
bool prevTagWasElement = false;
while( readOn && this.reader.NodeType != XmlNodeType.Text )
{
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.