1
Chapter 19
Container classes
1. Introduction
Abstraction and encapsulation in object-oriented programming refer to the concept of hiding details.
Abstraction does so at the design level while encapsulation hides details at the implementation level.
When we design a system, we will talk about an operation without discussing its details – we will do
so when we get there. Details are factored out so that we can focus on a few things at a time.
Encapsulation, on the other hand, refers to the fact that we hide details from a class user so that they
can focus on the usage thereof without worrying about how it works. For example, we can use the
built-in .Net method [Link] easily, but we do not know what algorithm Microsoft uses to do the
sorting and we do not worry about it – as long as it does its job correctly and efficiently.
Container classes are about encapsulation. We develop a class as a container for another class or
variable and then we only reveal the essential methods and properties to the class users. Mostly, the
container class will encapsulate the details of some kind of list structure, e.g. an array or a list (refer
to Chapter 10 again). The container class will enforce applicable business rules which will relieve the
class user of doing so. The container class can also provide aggregate methods about the private list,
for example the sum or average of specific fields of the underlying class.
It is important to note that we work on three levels: The class of the underlying elements, the container
class and the user (or client) class. Sometimes the underlying elements can be of a primitive type, e.g.
int or double or string and we don't implement a separate class for it. Don't confuse user in this
context with that of the end user of a program. We can also refer to the user class as the client class.
Often, the default Program class that Visual Studio creates, is renamed to Client.
2. Simple example
In this example, the underlying class CStudent, is an abstraction of the details of a student. Note the
use of the ToString method to provide a concatenated string with a student's details. It is important
that underlying classes and container classes never provide direct output to the user interface.
[Link] and [Link] statements are taboo – that is left to the class user.
The DOB and Modules classes are helper classes and not necessary for understanding the concepts of
this chapter. See the complete program in the student resource pack for their details.
[DefaultProperty("Name")]
public class CStudent
{
public string StudentNumber { get; private set; }
public string Name { get; set; }
public string Surname { get; set; }
public DOB dob {get; set;}
public decimal ClassFees { get; set; }
public List<Modules> lstModules { get; set; }
public CStudent(string sStudentNumber)
{
StudentNumber = sStudentNumber;
dob = new DOB();
Copyright: PJ Blignaut, 2020
2
lstModules = new List<Modules>();
} //Constructor
public override string ToString()
{
string s = "\tStudent number: " + studentNumber + "\n"
+ "\tName and surname: " + Name + " " + Surname + "\n"
+ "\tDOB: " + [Link]() + "\n"
+ "\tAge: " + [Link]() + "\n"
+ "\tClass fees: " + [Link]("C") + "\n"
+ "\tModules: ";
foreach (Modules module in lstModules)
s += module + " ";
return s;
} //ToString()
} //class CStudent
Listing 19.1.1: Class that will be contained inside the container class
We have an interface, IStudents, that exactly defines the public members that should be available to
class users. The container class inherits from this interface and must implement all the specified
methods and properties.
Since the class user cannot access the list of underlying elements, we need to give them a way of
knowing how many elements are in the list through the Count property. An interface only specifies
the public members of a class. Since we do not want the class user to change the value of the Count
property, the interface do not specify a set part.
The TotalClassFees method is an aggregate method that will return the sum of the class fees of
individual students. The indexers are identified by the keyword this and will be discussed in the next
section.
public interface IStudents
{
bool Add(CStudent student);
int Count { get; }
CStudent this[string sStudentNumber] { get; set; }
CStudent this[int i] { get; set; }
decimal TotalClassFees();
IEnumerator<CStudent> GetEnumerator();
} //IStudents
Listing 19.1.2: Interface to the container class
The container class, Students, encapsulates (hides) a private list of students. We do not want to
burden the class user with the details of adding students to the list or accessing the details of a specific
student in the list directly. So we provide public methods and properties to a class user to do this
without showing them how it is done.
public class Students : IStudents
{
//Collection
private List<CStudent> lstStudents; //Note that the list is private. All list operations
//are encapsulated in the class. The class user
//cannot access the objects in the list directly.
//Since the class user cannot access the list, we need to give them a way of knowing
//how many elements are in the list. Note that the set part is private: The class
//user cannot change its value from outside.
public int Count { get; private set; }
Copyright: PJ Blignaut, 2020
3
//Constructor
public Students()
{
lstStudents = new List<CStudent>();
Count = 0;
} //Constructor
//Indexers
public CStudent this[string sStudentNumber]
{
get { return [Link](s => [Link] == sStudentNumber); }
set
{
CStudent student = [Link](s => [Link] == sStudentNumber);
if (student != null)
student = value;
}
} //Indexer
public CStudent this[int i]
{
get { return lstStudents[i]; }
} //Indexer
//Methods
public bool Add(CStudent student)
{
if ([Link] == 10) //Check if the student number is valid
{
[Link](student); //Add the element to the list
Count = [Link]; //Update the value of the counter
return true;
}
return false;
} //Add
public decimal TotalClassFees()
{
return [Link](s => [Link]);
} //TotalRemuneration
} //class Students
Listing 19.1.3: Container class
The class user (see Listing 19.1.4) creates an instance of the container class and then uses the members
of the container class to access the underlying list of students. They cannot access the List<CStudent>
variable directly since it is private. Notice how the rule that the student number should be exactly 10
characters is implemented. The details of the rule is specified in the Add method in the container class.
The Add method returns a bool which is then used by the class user to provide the necessary feedback
to the end user.
using System;
using [Link];
class Program
{
static void Main(string[] args)
{
//Instantiate container class for list of employees
Students students = new Students();
Copyright: PJ Blignaut, 2020
4
//Add a student object to the container class and assign values to its
//properties through the string indexer
if ([Link](new CStudent("2019000001")))
{
students["2019000001"].Name = "John"; //Use string indexer
students["2019000001"].Surname = "Mitchell";
students["2019000001"].ClassFees = 10000;
students["2019000001"].dob = new DOB([Link]("1994/07/12"));
students["2019000001"].[Link](Modules.CSIS1614);
students["2019000001"].[Link](Modules.CSIS1624);
students["2019000001"].[Link](Modules.CSIS1664);
}
else
[Link]("Invalid student number");
//Another way
//- Create a local instance of the contained class and assign its properties
CStudent student = new CStudent("1234");
[Link] = "Paul";
[Link] = "Johnson";
[Link] = 2000;
[Link] = new DOB([Link]("1994/06/30"));
[Link](Modules.CSIS1553);
//- Now add the entire instance to the list
if ()
[Link]("Invalid student number");
//Use the int indexer
//– Not advised because the class user would not necessarily know what the index is.
if ([Link](new CStudent("2019000002")))
{
students[1].Name = "Mike"; //Use int indexer
students[1].Surname = "Johnson";
students[1].ClassFees = 2000;
students[1].dob = new DOB([Link]("1991/07/21"));
students[1].[Link]
(new Modules[] { Modules.CSIS2614, Modules.CSIS2624, Modules.CSIS2664 });
}
//List all students
for (int i = 0; i < [Link]; i++)
{
[Link](students[i].ToString()); //Use indexer
[Link]();
} //foreach
[Link]("\tTotal class fees: " + [Link]().ToString("C"));
[Link]("\n\nPress any key to continue ...");
[Link]();
} //Main
} //class Program
Listing 19.1.4: Using the container class
3. Indexers
You would have noticed the implementation and usage of indexers in Listings 19.1.3 and 19.1.4
respectively. An indexer is a special type of property that provides the class user with access to
elements in the private list in the container class. Comment them out and inspect the errors that you
get in the class user's environment.
Indexers are identified by the keyword this and a parameter between square brackets. They behave
like normal properties with a get and optional set part. The get part allows you to return an element
Copyright: PJ Blignaut, 2020
5
from the list to the class user and the set part allows you to overwrite the content of an existing
element. The class of the return type is that of the elements in the private list.
The indexers can be overloaded with either int or string (or something else) parameters. For the
version with a string parameter, it will help if you are familiar with Linq (Language-Integrated
Queries) and lambda expressions to access the elements. You can get around it with a sequential
search through the list, but Linq is so much more convenient if you know it.
public CStudent this[int i]
{
get { return lstStudents[i]; }
}
public CStudent this[string sStudentNumber]
{
get { return [Link](s => [Link] == sStudentNumber); }
set
{
CStudent student = [Link]
(s => [Link] == sStudentNumber);
if (student != null)
student = value;
}
}
4. Enumerations
You would have noticed that we used a for loop to traverse through the elements in the list in Listing
19.1.4 above. In order to use a foreach structure in the class user's environment, we have to add the
following method to the container class. The yield return statement returns the current element and
then comes back here to get the next element until all elements in the list are returned. Note that the
GetEnumerator method is generic and we have to provide the class of the underlying elements.
public IEnumerator<CStudent> GetEnumerator()
{
foreach (CStudent student in lstStudents)
yield return student;
} //GetEnumerator
We can now replace the for loop of the class user with the following. This does not make use of any
one of the indexers.
//Output using the enumerator
foreach (CStudent student_ in students)
{
[Link](student_.ToString());
[Link]();
}
5. Generic classes
A container class can also be generic. It should not be necessary to specify the class or type of the
elements of the underlying list. This will allow a class user to use the same implementation of a
container class for various applications. Listing 19.2.1 shows an example of such a class. The class
user must specify the exact type or class for T when an instance of the class is created. Note that this
generic type is also used as the type of elements in the underlying private list. The return types of the
indexers and the parameters in the Add and Remove methods are also of type T. We can also use an
array of T as in AddRange below.
Copyright: PJ Blignaut, 2020
6
public class Container<T> : IEnumerator
{
private List<T> lstElements;
private int idxCurrent = -1;
public int Count { get; private set; }
//Constructor
public Container()
{
lstElements = new List<T>();
idxCurrent = -1;
Count = 0;
} //Constructor
//Indexer
public T this[int i]
{
get { return lstElements[i]; }
set { lstElements[i] = value; }
} //Indexer
//Manipulation
public void Add(T element)
{
[Link](element);
Count++;
} //Add
public void AddRange(T[] elements)
{
[Link](elements);
Count = [Link];
} //AddRange
public void Remove(T element)
{
if (Count > 0)
{
[Link](element);
Count--;
}
} //Remove
public void RemoveAt(int i)
{
if (i >= 0 && i < Count)
{
[Link](i);
Count--;
}
} //RemoveAt
#region for IEnumerator
public IEnumerator<T> GetEnumerator()
{
foreach (T element in lstElements)
yield return element;
} //GetEnumerator
public void Reset()
{
idxCurrent = -1;
} //Reset
Copyright: PJ Blignaut, 2020
7
public bool MoveNext()
{
if (++idxCurrent < [Link])
return true;
else
return false;
} //MoveNext
public object Current
{
get { return lstElements[idxCurrent]; }
} //Current
#endregion IEnumerator
} //public class Container<T>
Listing 19.2.1: A generic container class
You would have noticed that we added the interface IEnumerator to the class in Listing 19.2.1.
Besides the GetEnumerator method which we discussed previously, this interface also enforces
implementation of the methods Reset, MoveNext and Current. We also added the private int
idxCurrent member to keep track of the current element in the list. Using these extra members, the
class user can step through the underlying list one element at a time. Note that the Current member
returns an object since the interface as originally defined in .Net does not know what the class of the
underlying elements would be. The class user would have to cast the returned value to the desired
type if necessary. The MoveNext method returns a bool which would allow the class user to know if
there are remaining elements in the list.
Listing 19.2.2 shows a possible usage of our generic container class. Note that since we are dealing
with primitive types in this example, we don't have to define an underlying class, but nothing prevents
us from defining a class for CCar and then creating a container for CCar for example.
static void Main(string[] args)
{
//Create container for ints
Container<int> intContainer = new Container<int>();
//- Add elements to container
[Link](1);
[Link](3);
[Link](5);
[Link](7);
//- Print elements in container (Using the enumerator)
[Link]("\tElements in intContainer (using enumerator):");
foreach (int i in intContainer)
[Link]("\t" + [Link]());
[Link]();
//Create container for strings
Container<string> stringContainer = new Container<string>();
//- Add elements to container
[Link](new string[] { "John", "Mike", "Susan", "Dan" });
//- Print elements in container (Using of indexer)
[Link]("\tElements in stringContainer (using indexer):");
for (int i = 0; i < [Link]; i++)
[Link]("\t" + stringContainer[i]);
[Link]();
Copyright: PJ Blignaut, 2020
8
//- Print elements in container (Using MoveNext)
[Link]("\tElements in stringContainer (using MoveNext):");
[Link]();
while ([Link]())
[Link]("\t" + [Link]);
[Link]();
[Link]("\tElements in stringContainer (use MoveNext - 2nd time):");
[Link](); //Comment this out and see what happens
while ([Link]())
[Link]("\t" + [Link]);
[Link]();
[Link]("\n\tPress any key to exit ...");
[Link]();
} //Main
Listing 19.2.2: Using a generic container class
6. Operator overloading
It happens quite often that we want to model types for which no built-in type or class exists in C#.
We can build our own class, but then we also have to replace (overload) the meaning of the standard
+, -, *, / symbols with another meaning. For example, it is not straight forward to do 11012 + 10012
in C#. .Net has a class Convert that we can use, but we can also create our own class, Bin, and
redefine the operators to function as expected. In Listing 19.3 we have an example of a class
Fraction and in Listing 19.4 we have a class Bin. We can also define classes for hexadecimal
numbers, sets, matrix operations, etc.
6.1 Fractions example
2 1 8
When we add fractions, for example 3 + 4 , we have to get the common denominator and then 12 +
3 11
= 12. Or for multiplication we have to multiply the numerators with each other and then the
12
2 1 2 1
denominators, for example 3 ∗ 4 = 12 = 6.
6.2 Structure of the container class
In the example below, we create a class Fraction and use two private int variables to represent the
numerator and denominator of a fraction.
class Fraction
{
//Fields
private int numerator;
private int denominator;
6.3 Instantiation of an instance of the new class
The constructor of the class takes the numerator and denominator as two separate parameters and
assign them to the private members:
Copyright: PJ Blignaut, 2020
9
public Fraction(int numerator, int denominator)
{
if (denominator == 0)
{
throw new Exception("Cannot have zero denominator.");
}
[Link] = numerator;
[Link] = denominator;
} //Constructor
We can also add a static Parse method that will allow the class user to instantiate a new instance of
the class by providing a string such as "1/2".
public static Fraction Parse(string s)
{
string sNumerator = [Link](0, [Link]("/"));
string sDenominator = [Link]([Link]("/")+1);
int numerator, denominator;
if ( [Link](sDenominator, out denominator)
&& [Link](sNumerator, out numerator))
return new Fraction(numerator, denominator);
else
return new Fraction(0, 1); //Return 0
} //Parse
6.4 Overloading the operators
The symbols +, -, * and / are assigned with a new meaning (overloaded) in the container class. This
is done through static methods and the operator keyword. Only the method for + is given here –
please look at Listing 19.3 for the others.
public static Fraction operator + (Fraction f1, Fraction f2)
{
return new Fraction([Link] * [Link] + [Link] * [Link],
[Link] * [Link]);
} //operator +
6.5 Comparison operators
The == operator should also be overloaded to return a bool value to indicate if two members of the
1 2
class are equal. Note that 2 and 4 are equal and our class should care for that.
public static bool Equals(Fraction f1, Fraction f2)
{
f1 = [Link]();
f2 = [Link]();
return ([Link] == [Link] && [Link] == [Link]);
} //Equals
The Reduce method is used to reduce a fraction to its simplest form. The details of the method can
be found in Listing 19.3.
Note that if the == operator is provided, it is compulsory to also provide a != method. This is simply
public static bool operator !=(Fraction f1, Fraction f2)
{
return !(f1 == f2);
} //operator !=
Copyright: PJ Blignaut, 2020
10
We can (and should) also overload the > and < operators:
public static bool operator > (Fraction f1, Fraction f2)
{
return [Link] * [Link] > [Link] * [Link];
} //operator >
public static bool operator <(Fraction f1, Fraction f2)
{
return [Link] * [Link] < [Link] * [Link];
} //operator <
6.6 ToString
As always, it is essential to provide an overridden ToString method:
public override String ToString()
{
if (numerator == 0)
return "0";
if (denominator == 0)
return "Infinite";
String sign = "";
if (numerator * denominator < 0)
sign = "-";
if (denominator == 1)
return sign + [Link](numerator);
return sign + [Link](numerator) + "/" + [Link](denominator);
} //ToString()
6.7 Usage of the class
We can use this new class as follows:
//Input
Fraction f1 = new Fraction(2, 5); //Using the constructor to instantiate
Fraction f2 = [Link]("1/2"); //Using the static Parse method to instantiate
//Addition
Fraction f3 = f1 + f2; //Using the new meaning of +
[Link]("\t" + f1 + " + " + f2 + " = " + f3 ); //Implicit usage of ToString
See Listing 19.3 for a more complete example of how the Fraction class can be used.
7. Events and event handling
7.1 Introduction
We discussed the concept of delegates in Chapter 17. You should revise that section again before
proceeding with this one.
Copyright: PJ Blignaut, 2020
11
One of the biggest advantages of delegates is the declaration of events
that can be handled by a class user. An event is something that happens
in the container class, akin to a mouse click. The figure to the right shows
some of the events of a button on a form. There are many events that
may occur, but we have written code for the Click event only. This
method, btnClose_Click, handles the event. Consider for example the
MouseMove event on a button: Every time that the mouse moves, the event
is triggered, but if we are not specifically interest in the event, we do not
write a handler for it. An event can be seen as a property of an object
while an an event handler is a method.
7.2 What is done in the container class
Consider the example of a container class that models a hard disk (HD). Listing 19.5.1 shows only
part of the example that is available in the student pack. The HD contents is modelled as a list of
bytes. Its capacity can be set to a maximum number of bytes. The class user should not have to keep
track of the hard disk contents – the hard disk should do that itself. The hard disk should then notify
the class user when it becomes full or when some other events occur.
The first thing to do, is to define the potential events as delegate types. Note that the delegate types
are classes themselves. Step 2 is to declare a special kind of instance of these delegate types, namely
events. The event objects must be public and the declaration cannot be inside a method. Note that the
event object is only declared – not instantiated.
In Step 3, we invoke (trigger/raise) an event. That is done, for example, when the hard disk becomes
full and the class user must be notified. It is left for the class user to handle the event or just ignore it.
If the user does not handle the event, it means that the event object is not instantiated and we will run
into a runtime error. For this reason, it is essential to also check if the object exists. Note the alternative
syntax to check if the event handler is null and invoke the event in one step.
//Step 1: Define the delegate type to specify the event handler signature
public delegate void delOnFull();
class CHardDisk
{
//Private data member to contain hard disk contents
//This cannot be public because then the class user will
//have the responsibility to check if it is full.
private List<byte> Contents { get; set; }
//Private data member to hold max capacity
private int Capacity;
//Step 2: Event declaration (reminiscent of an object)
public event delOnFull OnFull;
//Constructor
public CHardDisk(int Capacity)
{
[Link] = Capacity;
Contents = new List<byte>();
} //Constructor
//Public accessor of contents
public List<int> GetContents()
{
return Contents;
} //GetContents
Copyright: PJ Blignaut, 2020
12
//Public method to add content
public void Add(byte element)
{
if ([Link] < Capacity)
{
[Link](element);
//Step 3: Warn the user that this was the last element that could be added
if ([Link] >= Capacity && OnFull != null)
OnFull(); //Short for [Link]();
//Alternatively
//if ([Link] >= Capacity)
// OnFull?.Invoke();
} //if
} //Add
} //class CHardDisk
Listing 19.3.1: A container class for a hard disk to illustrate the declaration and invoking
of events. The full class is available in the student pack.
7.3 What is done in the class user
In the listing below, a form class is shown that will be the user of the HD container class. Contents is
added to the hard disk when the user clicks the Add button (btnAddContents_Click). Again, this is
only part of the example that is available in the student pack.
Step 4 entails subscribing an event handler to an event (using +=). This will also instantiate the event
so that the event object is no longer null. Step 5 is about developing the method (event handler) that
will be executed when the event occurs.
In Step 6, we unsubscribe (using -=) the event handlers from the event objects. Remember that when
a form is closed, it still exists. If we do not unsubscribe an event handler when a form is closed, a
second instance of the event handler will be subscribed to the event (+=) when the form is opened
again. This means that the code will be executed twice when the event is triggered.
public partial class CfrmHardDisk : Form
{
//Hard disk object
private CHardDisk HD;
//Form constructor
public CfrmHardDisk()
{
InitializeComponent();
//Instantiate hard disk object with capacity for 10 bytes
HD = new CHardDisk(10);
//Step 4: Connect (subscribe) event handlers to events
[Link] += HD_OnFull;
} //Constructor
//Event handler for FormClosing
private void CfrmHardDisk_FormClosing(object sender, FormClosingEventArgs e)
{
//Step 6. When a form is closed, it still exists and therefore the HD object
//still exists and therefore these events will still be triggered. It is
//essential to disconnect (unsubscribe) the event handlers explicitly.
[Link] -= HD_OnFull;
HD = null;
} //CfrmHardDisk_FormClosing
Copyright: PJ Blignaut, 2020
13
private void btnAddContents_Click(object sender, EventArgs e)
{
Random rnd = new Random([Link]);
byte b = (byte)[Link]();
[Link](b);
} //btnAddContents_Click
//Step 5
private void HD_OnFull()
{
[Link]("Hard drive full");
[Link] = false;
} //HD_OnFull
} //class CfrmHardDisk
Listing 19.3.2: A form class using the container class for a hard disk.
7.4 Summary of steps
The whole procedure of declaring and using events, can be summarised in 6 steps:
Anywhere in the code, but preferably in the same file and above the container class:
1. Define the delegate which will define the signature of the event handler.
In the container class:
2. Declare the event objects. They must be public.
3. Trigger (invoke) the events when necessary
In the class user:
4. Develop the code for the event handlers
5. Assign (subscribe) the event handlers to the event objects
6. Unsubscribe the event handlers
7.5 The generic EventHandler delegate type
Instead of defining the delegate type explicitly in Step 1, we can use the generic built-in EventHandler
delegate type. If we define the delegate types ourselves, we have control over the event handler
signature, but if we use the EventHandler delegate type, the signature is fixed (which is not necessarily
a bad thing). Since the delegate type is built-in, we don’t have to define it explicitly, but if we would,
it would look like this:
public delegate void EventHandler (object sender, EventArgs e);
In Step 2, the event object can then be declared like this:
public event EventHandler OnRemove;
When we invoke the event in Step 3, we should be aware of the fixed signature, for example:
Copyright: PJ Blignaut, 2020
14
public void RemoveAt(int index)
{
if (index >= 0 && index < [Link])
{
OnRemove?.Invoke(Contents[index], [Link]);
[Link](index);
}
} //RemoveAt
In this example, we have decided to use the element that was removed from the contents as the sender
object. The class user can then use this object in Step 5 to identify the element that was removed:
private void HD_OnRemove(object sender, EventArgs e)
{
[Link](sender);
[Link](sender + " was removed from the hard disk.");
[Link] = true;
} //HD_OnRemove
8. Compiling as a DLL
It is possible to compile a container class as a separate project. That will allow people to work in
teams where one person will develop the container class and another using it. The developer of the
container class compiles the project as a dll as opposed to be part of an exe. This dll file can then be
copied to the machine of the user thereof. The user includes it as a reference to their project and add
a using directive to the namespace of the container class to their own project. The two projects
(container and user) can be in the same solution or different solutions. If the container class is
developed and compiled in a separate solution, the user does not have access to the code thereof –
only the public members (methods and properties) will be visible (through IntelliSense) and
accessible. That is actually what we want to achieve – we don’t want class users to tamper with its
content.
9. Summary
The following key concepts were discussed in this chapter:
• Abstraction and Encapsulation
• Underlying class, container class and class user
• Interface
• Indexers
• Enumeration
• Generic classes
• Operator overloading
• Events and event handling
• Class library
Copyright: PJ Blignaut, 2020