Components and Containers

Summary

Containers, Sites, and Components

Recall that a component derives from System.ComponentModel.Component, usually has no GUI interfaces , whereas a Control derives from System.Windows.Forms.Control and usually has GUI elements associated with it. Note the following basic points about component containers:

Container are extensible. For example, you could create your own container class that inherits directly from System.ComponentModel.Container and override the virtual members of IContainer appropriately. The following container specializes System.ComponentModel.Container by only allowing the addition of a specific type of components:

public class EmployeeContainer : Container
{
    // Override the IContainer.Add virtual method to allow adding of Employee objects only
    public override void Add(IComponent component)
    {
        if (component is Employee)
            base.Add( component);
        else
            throw new InvalidOperationException("Only components of type Employee can be added");
    }

    /* Add any other required methods and properties */
}

Recall that any component that derives from System.ComponentModel.Component or implements IComponent will have the Site property. This property is used to interact between the component and its container. This property represents the logical site of the component. A component that is not contained by a container will return null for its Site property. In other words, the Site property allows you to retrieve a reference to the container interface through Site.Container property.

To summarize, through a container you can track your components, communicate with them via their Site property and provide a means of common disposal after they are no longer needed. Using a container is very easy:

System.ComponentModel.Container container = new System.ComponentModel.Container();
container.Add( component 1);
container.Add( component 2);
container.Remove( component 1);

Communication between Container and Components

A container is considered as the focal point for communication between a client application and its components. Recall that a container that implements the IContainer interface has Add, Remove, and Components class members. The Components property can be used to retrieve a specific component from the container:

IComponent component = MyContainer.Components( nIndex );

Note how the Components property returns an IComponent interface. This will only allow you to access methods implemented by the IComponent interface.

Likewise, a component can interact with its container using the component's own Site property:

IContainer container = MyComponent.Site.Container;

Complex Components

Object Model

A Class Hierarchy describes class inheritance. It shows how classes are derived from other base classes to specialize their behavior. Contract this to an Object Model which is a collection of hierarchies that describe containment. In other words, an object model shows how complex objects, like the DataSet, contain collections of other components, like DataTable and DataRelation.

When designing components, you need to consider how complex you component will be and whether it needs an object model. Here are the general recommendations to keep in mind when designing object models for complex components:

Nested Classes in Complex Components

Complex components have many subordinate components. For example, a DataSet object consists of many DatarRelation and DataTable objects which turn consist of many DataRow, DataColumn object, etc.  Nested classes are classes whose definition is completely nested within another class declaration. They are useful for providing objects required by a particular class to function, but have no stand-alone functionality. For example, a  PC object may have many subordinate object like Keyboard object, Screen object, HD object, and so on without which the PC object may not function. The average user of the PC object would probably not need to worry about creating Keyboard and Screen objects - he/she is only interested in the PC object. Here is how to declare an enclosing class:

public class PC
{
    ...
    private class Keyboard { ... }
    private class Screen   { ... }
    private class HD       { ... }
    ...
}

Nested classes are useful in this case where the PC class contains the implementation of all its nested classes. This way the PC class can create and manage its objects while hiding the details of the implementation from the client. However, the client may wish to interact with these subordinate objects (HD, Screen, and Keyboard). In this case, they can be exposed as part of the object model by specifying the appropriate access level.

The access level of the nested class is implicitly at least as accessible as the containing class. If the nested class is private as in the example above, the class can only be called by the containing class. If the nested class is public, its accessibility is determined by the accessibility of the containing class: If the containing class is public, the public nested class can be accessed by any one. If the containing class is internal, the public nested class can only be called by members of the assembly.

Here are some design recommendation for using nested classes in components:

Properties and Collections in Object Models

A standard implementation in object models found in the .NET Framework is that container objects will have properties that either return references to collections themselves or return references to the contained objects.  An example of returning references to collections would be the DataSet and the DataTable classes. The DataSet class has a property called Tables that returns a reference to the DataTableCollection collection:

DataTableCollection dtColl = MyDataSet.Tables;

And each DataTable in the DataTableCollection collection has a Columns property that returns a DataColumnCollection collection:

DataTable MyDataTable       = dtColl["Customers"];
DataColumnCollection dcColl = MyDataTable.Columns;

An example of returning references to contained objects would also be the DataSet object which contains a CultureInfo object exposed via the Locale property:

CultureInfo ci = MyDataSet.Locale;

It makes sense to organize the object model for a DataSet in this way where a DataSet object has a collection of Tables and each Table has a collection of Columns. A functioning DataSet cannot exist without these objects because it is these objects that make up a DataSet.

Properties in Object Models

There are two approaches to linking contained object to the container object:

public class PC
{
    // Nested classes
    internal class HardDrive { ... }
    ...

    // Data members
    private HardDrive hd = new HardDrive();
    ...

    // Properties
    public HardDrive HD
    {
        get { return hd; }
    }
    ...
}

public class PC
{
    // Nested classes
    internal class Key { ... }
    ...

    // Data members
    private ArrayList FunctionKeys = new ArrayList();

    // Properties (see section below for limitations of this approach)
    public ArrayList Keys
    {
        get { return FucntiobKeys; }
    }
    ...
}

Collections in Object Models

As stated previously, if a complex object has an indefinite number of instances of a dependent object, then you it is best to implement the linkeage with a collection rather than a single property. The .NET Framework supplies many different types of collections to track and managed contained objects. System.Collections namespace contains many useful collections such as ArrayList, BitArray, Stack, Queue, and others.

Also as stated in the previous section, you can implement a simple property as follows:

public class PC
{
    // Nested classes
    internal class Key { ... }
    ...

    // Data members
    private ArrayList FunctionKeys = new ArrayList();

    // Properties
    public ArrayList Keys
    {
        get { return FucntiobKeys; }
    }
    ...
}

However, this implementation suffers from some problems:

These issues should be addresses by implementing your own custom collection that derives form System.Collecitons.CollectionBase. This approach addresses the above two issues by exposing only the methods that you want and by implementing a collection that is strongly typed (accepts objects of certain type only). See Creating You Own Collection Class below.

Another less robust approach would be not to expose the collection directly but rather using wrappers to return selected methods of the collection. The following example illustrates:

public class PC
{
    // Nested classes
    internal class Key { ... }
    ...

    // Data members
    private ArrayList FunctionKeys = new ArrayList();

    // Wrappers around the FunctionKeys collection
  
public FunctionKey GetFunctionKey( index n )
    {
        return FunctionKeys[n];
    }

   public FunctionKey GetFunctionKey( string name )
    {
        return FunctionKeys[name];
    }

    public void AddFunctionKey( FunctionKey key )
    {
        FunctionKeys.Add( key );
    }

    ...
}

Creating You Own Collection Class

The .NET Framework provides several collection classes in the System.Collections namespace. Collection classes such Stack, Queue and Dictionary are specialized classes that address specific requirements. To implement your own collection class that meets your own requirements you will often have to inherit either from CollectionBase or DictionaryBase. These are abstract classes for strongly types collections/dictionaries.

As mentioned CollectionBase is an abstract class but already provides some (!) implementation by inheriting from IList and ICollection interfaces and implementing some (!) interface members. For example, CollectionBase already implements Clear and Count and also maintains a protected property called List which it uses for internal storage and organization.

In the paragraph above I emphasized that CollectionBase implements some interface members. How could a class implement some interface members and still compile? When a class inherits from an interface, the inheriting class must implement all and not some of those members. So what is going on here? The answer lies with Explicit Interface Implementation. If you look at CollectionBase documentation you will note that while it implements some IList and some ICollection interface members, the remainder of these interface members are implemented using explicit interface implementations (Recall: ICollection defines size, enumerations, and synchronization methods for all collections. IList specializes ICollection and represents a collection of objects that can accessed by index)

Explicit Interface Implementation was covered fully in C# Types here. The main points however are:

These above points are illustrated here:

// A collection class that extends CollectionBase class 
public class EmployeeCollection : System.Collections.CollectionBase
{
    /* Data members */
    private string strName; 

    /* Properties */
    public string Name
    {
        get { return strName; }
    }
}

// Create the EmployeeCollection specialized collection
Components.EmployeeCollection coll = new Components.EmployeeCollection();

// You can only add objects using explicit interface implementation techniques 
coll.Add( "hello" );            // Error: 'Components.EmployeeCollection' does not contain a definition for 'Add'

IList list = (IList)coll;       // OK
list.Add( "hello" );

The following complete example uses CollectionBase to create a specialized collection that only accepts objects of type Employee instead of accepting and exposing contained objects as Object.

// The contained objects to be maintained by EmployeeCollection
public class Employee
{
    private string strName;
    public string Name
    { 
        get { return strName; }
        set { strName = (string)value; }
    }
}

// EmployeeCollection class that only contain objects of type Employee
public class EmployeeCollection : System.Collections.CollectionBase
{
    /* Constructors */
    public EmployeeCollection() {}

    /* IList interface implementation */

    // The Add method declaration permits only Employee objects to be added
    public void Add( Employee obEmp)
    {
        this.List.Add( obEmp );
    }

    // Implement a Remove method
    public void Remove( int nIndex )
    {
        // Check if index is valid
        if ((nIndex < 0) || (nIndex > this.Count - 1))
            throw new ArgumentOutOfRangeException( "nIndex", "Index is not valid");

        this.List.RemoveAt( nIndex );
    }

    // Implement an indexer that returns only Employee objects (and not object of type Object)
    public Employee this[int nIndex]
    {
        get { return (Employee)this.List[nIndex]; }
        set { List[nIndex] = value; }
    }
}

private void CollectionBase_Click(object sender, System.EventArgs e)
{
    // Create some employee obejcts
    Employee emp1 = new Employee();
    emp1.Name = "Employee1";

    Employee emp2 = new Employee();
    emp2.Name = "Employee2";

    // Create our specialized collection
    Components.EmployeeCollection coll = new Components.EmployeeCollection();

    coll.Add( emp1 );
    coll.Add( emp2 );

    string strName1 = coll[0].Name;
    string strName2 = coll[1].Name;

    coll.Remove( 0 );
    coll.Remove( 1 );     // Error. There is only item in the collection now
}