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
.