Tek-Tips is the largest IT community on the Internet today!

Members share and learn making Tek-Tips Forums the best source of peer-reviewed technical information on the Internet!

  • Congratulations Chris Miller on being selected by the Tek-Tips community for having the most helpful posts in the forums last week. Way to Go!

How to create strongly-typed collections of objects

Code Examples

How to create strongly-typed collections of objects

by  chiph  Posted    (Edited  )
Strongly-typed collections in C#
----------------------------------

(c) 2003 Chip Holland
Permission to copy is granted as long as credit is given to me and
Tek-Tips.com

This FAQ addresses the weakly-typed collections found in typical .NET
code. Specifically, making use of the framework's collection classes
can result in two wholly different data types being stored in the
same collection (Invoice and Customer objects, for example). This is
because the FCL has to be everything to everybody -- as a result, the
FCL collections are usually of type Object, which permits the
programmer to store anything in the collection, even if they aren't
related.

If you look at how the FCL is organized, whenever you have a parent-
child relationship, where there can be multiple children, there is
almost always an intermediate childs object. Please excuse
the poor grammar, but in the FCL, this object is nearly always named
the plural of what it holds. So you end up with:

parent - children - child
kennel - dogs - dog
furniture - chairs - chair
Application - Threads - Thread


The middle object is what provides the strong typing that you need.
It will not allow you to put a cat object in a dogs collection.

Now... how to accomplish this in your own code? And how to do this
so your collection users can use the foreach(){} keyword to iterate
through your list, plus use array symmantics to access each item
directly? And how do you bind your collection to any of the
databound objects, such as a datagrid, datalist, and the like?


You could write your own collection by implementing IList or
ICollectionBase, but that's a lot of work. Better to encapsulate
one of the existing .NET framework collection types and provide
wrapper methods. We'll use the classic CS102 subject: a collection
of music albums.

Start by opening VS.NET, and starting a new Class Library project.
You can remove the references to System.Data and System.XML, you
won't be needing them. Rename the class to "Song" (do the class,
file & constructor). Add public member variables Title (a string) and
Length (a TimeSpan). Save your class. It should look like:
Code:
using System;

namespace ClassLibrary1
{
	/// <summary>
	/// Summary description for Song.
	/// </summary>
	public class Song
	{
		public Song()
		{
			//
			// TODO: Add constructor logic here
			//
		}

		public string Title;
		public TimeSpan Length;
	}
}

Now add a new class named "Songs". This class will need stuff from
System.Collections, so add a using statement for it. Declare a
private member variable of type ArrayList named m_SongList;
Instantiate m_SongList in your constructor. Next, open the class
view pane, and expand the tree to get to the Songs class. Right-
click and select "Add", then select "Indexer". You'll see the
Indexer wizard. Change the Indexer type from "int" to "Song" (you can
type over it). Enter a parameter named "index" and click the Add
button. Click the "Finish" button and you'll have a new method added
to your class, which should now look like this:
Code:
using System;
using System.Collections;

namespace ClassLibrary1
{
	/// <summary>
	/// Summary description for Songs.
	/// </summary>
	public class Songs
	{
		private ArrayList m_SongList;

		public Songs()
		{
			m_SongList = new ArrayList();
		}

		public Song this[int index]
		{
			get
			{
				return (Song)m_SongList[index];
			}
			set
			{
				m_SongList[index] = value;
			}
		}
	}
}

Note that the get property casts the return value to a Song
object before returning. Also note that the name of the property is
the keyword "this" that has square brackets on it like an array.
Which is what this method does - it allows your users to access the
contents of the private ArrayList m_SongList via an index. Also
note that if your users pass an invalid index in, the ArrayList itself
will throw an exception. You can check for valid values before
indexing into the ArrayList and raise your own exception if you choose.

As cool as this is, we still can't use this in a foreach(){} function
call. For that we need to implement an interface provided by the
.NET framework called IEnumerable. This interface has a method
named GetEnumerator that returns an object which implements another
interface named IEnumerator. Note the meaning differences in
the names - something which is enumerable returns something which is
an enumerator.

First step is to add the IEnumerable to the inheritance list of the
class definition. Since it's an interface, and not another class,
you still get to use your single-inheritance for something else
(nice!). Your class then looks like this:
Code:
using System;
using System.Collections;

namespace ClassLibrary1
{
	/// <summary>
	/// Summary description for Songs.
	/// </summary>
	public class Songs : IEnumerable
	{
		private ArrayList m_SongList;

		public Songs()
		{
			m_SongList = new ArrayList();
		}

		public Song this[int index]
		{
			get
			{
				return (Song)m_SongList[index];
			}
			set
			{
				m_SongList[index] = value;
			}
		}
	}
}

Open up the class viewer again and expand the "Bases and Interfaces"
branch. Locate IEnumerable in there, and right-click. Select "Add",
then "Implement Interface". The IDE will then add a new method to
your class that provides a template for the interface. You must go
in and customize it (we'll do that in a bit). Your class now looks
like:
Code:
using System;
using System.Collections;

namespace ClassLibrary1
{
	/// <summary>
	/// Summary description for Songs.
	/// </summary>
	public class Songs : IEnumerable
	{
		private ArrayList m_SongList;

		public Songs()
		{
			m_SongList = new ArrayList();
		}

		public Song this[int index]
		{
			get
			{
				return (Song)m_SongList[index];
			}
			set
			{
				m_SongList[index] = value;
			}
		}

		#region Implementation of IEnumerable
		public System.Collections.IEnumerator GetEnumerator()
		{
			return null;
		}
		#endregion
	}
}

The GetEnumerator method wants to return something that implements
the IEnumerator interface. To give it that something, we'll add a
private class. Private classes aren't used much, but this is a
perfect opportunity for one. Beneath the "#endregion" type in a new
class definition:
Code:
		private class SongEnumerator : IEnumerator
		{
			public SongEnumerator() 
			{
			}
		}

This class implements the IEnumerator like we want, but we need to
add the methods demanded by the interface. Open up the class view
like before, navigate to the SongEnumerator class, right-click and
select "Add"..."Implement Interface" like before. The IDE will
add a few methods which we must fill in. Your code should now look
like:
Code:
using System;
using System.Collections;

namespace ClassLibrary1
{
	/// <summary>
	/// Summary description for Songs.
	/// </summary>
	public class Songs : IEnumerable
	{
		private ArrayList m_SongList;

		public Songs()
		{
			m_SongList = new ArrayList();
		}

		public Song this[int index]
		{
			get
			{
				return (Song)m_SongList[index];
			}
			set
			{
				m_SongList[index] = value;
			}
		}

		#region Implementation of IEnumerable
		public System.Collections.IEnumerator GetEnumerator()
		{
			return null;
		}
		#endregion

		private class SongEnumerator : IEnumerator
		{
			public SongEnumerator() 
			{
			}

			#region Implementation of IEnumerator
			public void Reset()
			{
			}

			public bool MoveNext()
			{
				return true;
			}

			public object Current
			{
				get
				{
					return null;
				}
			}
			#endregion
		}
	}
}

For the Songs class to know about the SongsEnumerator, and for the
SongsEnumerator to have access to the private ArrayList in the Songs
class, you need to pass a reference to the this object to the
SongsEnumerator constructor. The SongsEnumerator then needs a place
to store that reference so we'll add a private member variable to
hold it. Your code should now look like this:
Code:
using System;
using System.Collections;

namespace ClassLibrary1
{
	/// <summary>
	/// Summary description for Songs.
	/// </summary>
	public class Songs : IEnumerable
	{
		private ArrayList m_SongList;

		public Songs()
		{
			m_SongList = new ArrayList();
		}

		public Song this[int index]
		{
			get
			{
				return (Song)m_SongList[index];
			}
			set
			{
				m_SongList[index] = value;
			}
		}

		#region Implementation of IEnumerable
		public System.Collections.IEnumerator GetEnumerator()
		{
			return new SongEnumerator(this);
		}
		#endregion

		private class SongEnumerator : IEnumerator
		{
			private Songs m_SongsRef;
			
			public SongEnumerator(Songs SongsRef) 
			{
				this.m_SongsRef = SongsRef;
			}

			#region Implementation of IEnumerator
			public void Reset()
			{
			}

			public bool MoveNext()
			{
				return true;
			}

			public object Current
			{
				get
				{
					return null;
				}
			}
			#endregion
		}
	}
}

We now need to fill in the code in the implementation of the
IEnumerator interface. The methods that got added are:
Reset() - called when the caller wants to start at the
beginning of the collection.
MoveNext - as the name implies, called when the caller wants
to move to the next item in the collection.
Current - a property, not a method, it's called to get the
value of the current item.

All this implies that we need a way to keep track of what our current
location in the ArrayList is. And to do that, we'll use a private
int variable to watch our location (named m_Location, cleverly
enough). The code in the Reset method then becomes really easy --
just set the location to -1 (collections always start at the position
before the actual start). MoveNext is almost as easy - just
increment the location, and then return false if we've gone beyond
the end of the data, or true if we're still in the midst of our
data. The Current property just returns the current item in the
collection. Note that it must be returned as an Object in order
to correctly implement IEnumerator. The code for your enumerator
class should now look like:
Code:
		private class SongEnumerator : IEnumerator
		{
			private Songs m_SongRef;
			private int m_Location;

			public SongEnumerator(Songs SongRef) 
			{
				this.m_SongRef = SongRef;
				m_Location = -1;
			}

			#region Implementation of IEnumerator
			public void Reset()
			{
				m_Location = -1;
			}

			public bool MoveNext()
			{
				m_Location++;
				return (m_Location <= (m_SongRef.m_SongList.Count - 1));
			}

			public object Current
			{
				get
				{
					return m_SongRef.m_SongList[m_Location];
				}
			}
			#endregion
		}

Note that like before, if the caller asks for the current value despite the
MoveNext method returning false, the ArrayList will throw an exception. You
probably ought to return a null in that situation:
Code:
			public object Current
			{
				get
				{
					if ((m_Location < 0) || (m_Location > 

m_SongRef.m_SongList.Count)) 
					{
						return null;
					}
					else
					{
						return m_SongRef.m_SongList[m_Location];
					}
				}
			}

All that's left now (or is it?) is to create an Album object
that will contain your Songs, and this is pretty easy:
Code:
using System;

namespace ClassLibrary1
{
	/// <summary>
	/// Summary description for Class1.
	/// </summary>
	public class Album
	{
		public string Artist;
		public Songs Songs;
		public DateTime DateReleased;
		public Album()
		{
			Songs = new Songs();
		}
	}
}

If you were to view your code in the object viewer (Ctrl+Alt+J),
you'll see the relationships, and everything looks great. Only...
there's no way to add a Song to the Songs collection. The m_SongList
is private (as it should be), and there's no Add or Remove method.
You could easily solve this by making the m_SongList public, but then
you're tied to an ArrayList. If you should want to implement your
collection of songs using a Queue-style collection, then all
your users would have to change their code, because Queue objects
don't have an Add method.

So you add your own Add and Remove method which insulates your users
from the fact that you used an ArrayList to store your list of songs.

To your Songs class, add two new methods:
Code:
		public void Add(Song song) 
		{
			// Ignore return value
			m_SongList.Add(song);
		}

		public void Remove(Song song)
		{
			m_SongList.Remove(song);
		}

And you're done. Well, for a basic implementation, sure. There's
a lot more you can do to dress it up - add parameterized constructors
to the Song class, add a "Create" method to the Songs
class so your users don't have to instantiate a Song object
before calling Add, and a lot more.

Another thing to investigate is adding more info to the XML Comments
found before the class and method declarations so that the
Intellisense info will be more useful to your users.

Register to rate this FAQ  : BAD 1 2 3 4 5 6 7 8 9 10 GOOD
Please Note: 1 is Bad, 10 is Good :-)

Part and Inventory Search

Back
Top