Implementing Components

Summary

Component Class Characteristics

When a class is used to create a component, the following class characteristics become more important and must be considered carefully:

Class Characteristics Notes
Component Name Class names should be short, descriptive so they can be easily organized.
Access Modifier Component classes should be always be made public.  Do not declare your components classes as private, internal, or protected
Base Class Unless you intend to implement IComponent yourself, your component class should derive System.ComponentModel.Component.
Namespace In the component assembly,  the namespace should be structured such that it reflects the internal organization of your component model. This helps users understand how to use your components.

Component Instancing

The creation of a component should be controlled as appropriate with the control's intended purpose. For example, some components can be created as usual by every body, while other controls should only be created by other components.

Public Constructors

In general, make a constructor public to allow users to create and use your control.

Internal Constructors

Classes an internal constructor can be created just like any other class using the new operator. However, note the following when creating objects with an internal constructor:

For example:

// Assembly1
public class MyComponent : System.ComponentModel.Component
{
    public MyComponent()
    {
        ...
    }

    internal MyComponent( int nID )
    {
        ...
    } 

    ...
}

// Assembly2
MyComponent ob1 = new MyComponent();        // OK
MyComponent ob2 = new MyComponent(nID);     // error CS0122: 'MyComponent.MyComponent(int)' is inaccessible due to its protection level

Therefore, you can invoke an internal constructor only form within the same assembly. When would you use an internal constructor? The internal modifier implies 'accessible only from within this program/assembly'. A typical application is when you want to have a component that should only appear as a member of a collection class. For example, in the following code, the Customers component is passed parameters for creating a new customer object (class with an internal constructor), and then returns this new customer object to the client. The client cannot create the customer object directly:

// Assembly 1
public class Customer
{
    internal Customer( string name, string address )
    {
        ...
    }
}

public class Customers
{
    public Customer Add( string strName, string strAddress )
    {
        // Create a new Customer object. Because we are in the same assembly as Customer, we can create Customer
        // objects as usual
        Customer obCustomer =  new Customer( strName, strAddress );

        // Then add the customer to some internal collection
        ...

        // Then return new object to client
        return obCustomer; 
    } 

    ...
}

// Assembly 1
Cusotmers obCustomers = new Customers();
Customer  obCustomer  = obCustomers.Add( "MyName", "MyAddress" );    // Customer object returned through Customers object

Private / Protected Constructors

A class (component) with a private or protected constructor cannot be created using new. A typical example would be a class (component) that had only static data and no instance data. Creating instances of such classes would waste memory. Another typical usage of private constructors is in the Factory design pattern, where you use a helper class to create the required class for you rather than create the class directly. See Design Pattern sections

Methods, Events, Properties, and Members in Components

Methods represent component functionality. Methods can be of two types: value-returning and non-value returning depending on the purpose of the function. Events are ways for your component to interact with the rest of the program. 

Events are raised in pre-determined conditions. Once an event is fired, it is handled by an event handler. Note that event implementations in components is the same as event implementations in custom controls except that components do not have a visual interface and hence should not respond to UI events. See Events in Introduction to Components.

Properties allows components to store, manipulate, and retrieve data. Properties are superior to data members because they allow developers to test and manipulate data (if necessary) before storing them. See Properties in C# Classes. Note the default properties allow to omit the property name. Once such example in C# is the indexer, which appears to a VB Programmer as a default Item property. Likewise, a VB default property appears in C# as an indexer.

Finally, saving the state of the component is done through serialization. This process walks an object-graph and converts it to a stream of bits that can be persisted where appropriate (database, file system, etc.). The following shows how to serialize and de-serialize an object:

public enum FormatterType
{
    BINARY,
    SOAP
}

public class Serializer
{
    public void SerlializeObject( object ob, string filename, FormatterType eType )
    {
        try
        {
            // Create the right formatter
            IFormatter obFormatter = null;
            if (eType == FormatterType.BINARY)
                obFormatter = new BinaryFormatter();

            if (eType == FormatterType.SOAP)
                obFormatter = new SoapFormatter();

            // Create a stream based on the file
            FileStream stream = new FileStream( filename, FileMode.Create );

            // Now serialize into the newly-created formatter
            obFormatter.Serialize( (System.IO.Stream)stream, ob ); 
        }
        catch( Exception e )
        {
            Trace.WriteLine( e.Message );
        }
    }

    public object DeserializeObject(string filename, FormatterType eType)
    {
        try
        {
            // Create the right formatter
            IFormatter obFormatter = null;
            if (eType == FormatterType.BINARY)
                obFormatter = new BinaryFormatter();

            if (eType == FormatterType.SOAP)
                obFormatter = new SoapFormatter();

            // Create a stream based on the file
            FileStream stream = new FileStream( filename, FileMode.Open );

            // Now serialize into the newly-created formatter
            object ob = obFormatter.Deserialize( stream );
            return ob;
        }
        catch( Exception e )
        {
            Trace.WriteLine( e.Message );
            return null;
        }
    }
}

Interfaces and Abstract Classes

Polymorphism in Components

Recall that polymorphism is the ability to have different run-time implementations for the same method based on the run-time type of its class. Polymorphism in components can be implemented in 3 ways:

// Interface re-implementation
public interface IZ
{
    void foo();
}

public class Z : IZ
{
    public void foo() {Trace.WriteLine("Z.foo()");}    // class Z implements IZ
}

public class Z2 : Z, IZ
{
    public void foo() {Trace.WriteLine("Z2.foo()");}  // class Z2 re-implements IZ
}

Some of the major benefit of interface polymorphism are:

Abstract Classes

Note: Abstract classes were covered from a C# perspective in C# Classes chapter.

Recall that abstract classes cannot be instantiated and are frequently either partially implemented or not implemented at all. One key difference between abstract classes and interfaces is that a class may inherit and implement an unlimited number of interfaces, but may inherit and implement only one base class (or any other kind of class.) A class derived from an abstract class can still implement an unlimited number of interfaces. Abstract classes are useful when creating components because they allow to specify unchangeable methods but leave the implementation of others until a specific implementation of a class is needed. For example, in the AbstractClass1 which is an abstract class, method bar() is already implemented and derived classes need not implement it. However, method foo() must be implemented by any deriving  class:

public abstract class AbstractClass1
{
    // This method is already implemented and the driving class need not re-implement it (but it can hide it with new)
    public void bar() { Trace.WriteLine("AbstractClass1.bar{}"); }

    // This method must be implemented by deriving classes
    public abstract void foo();
}

public class ConcreteClass1 : AbstractClass1
{
    // Must provide implementation of all abstract members of an abstract class.
    public override void foo()
    {
        Trace.WriteLine("ConcreteClass1.foo()");
    }
}

Interfaces

Note 1: Abstract classes were covered from a C# perspective in C# Types chapter.

Once an interface is written and used by clients, it should remain invariant (should not change) to protect applications written to use it. Therefore, do not change interfaces once published. When a published interface needs new enhancements, a new interface should be created. The currently used convention is to append an incrementing number to the end of the interface name (ISerialize, ISerialize2, ISerialize3, ...) to show that these interfaces are related.

When designing interfaces, the process of determining what methods, properties, and events should belong to the interface is called interface factoring.  In general, closely-related functions should be grouped in the same interface. However, note that too many functions in the interface will make it difficult to use, while dividing the parts of an interface too finely may result in extra overhead and diminished ease of use.

In general, it is much harder to go wrong in designing interfaces than in creating large inheritance trees. If you start small, you can have parts of the system running quickly. This ability to evolve a system by adding more interfaces allows you to gain Object-Oriented advantages.

Recall that an interface is a contract - it defines a set of methods, properties, and events that are to be found in any class that implements the interface. This ensures that if your class implements ISerialize for example, then any client class that expects to interact with ISerialize can (and should) interact with your class, regardless of what other functions your class may provide.

When using interfaces, keep the following points in mind:

Choosing between Interfaces and Abstract Classes

The choice of whether to design your component using abstract classes or interfaces can be a difficult one because both are useful for component interaction. For example, if a method takes an interface as an argument, then any (and this is the keyword here) object that implements that interface can be used an an argument:

// Define classes that implement IEngine
public class Car : IEngine
{ ... }

public class Boat : IEngine
{ ... }

// This method calls another method (StartEngine) that takes an interface as an argument
public void foo()
{
    // Simulate a car engine using the StartEngine method
    StartEngine( new Car() );

    // Simulate a boat engine using the StartEngine method
    StartEngine( new Boat() );
}

// Because this method takes an interface, any object that implements the IEngine interface can be passed in
public void StartEngine( IEngine engine )
{
    // Call methods on the engine interface (does not matter what object is implementing IEngine)
    engine.Start();
    ...
}

Here are the recommendation to help you decide on whether to use interfaces or abstract classes to provide polymorphism for components:

Example

The following is a simple example for declaring and implementing interfaces:

// Decalre an interface for dealing with customers
public interface ICustomer
{
    // Interface members are public by default
    string Name { get; set; }
    string Address { get; set; }
}

// Decalre an interface for dealing with account
public interface IAccount
{
    // Interface members are public by default
    void OpenAccount( ICustomer customer);
    void CloseAccount();
    void Deposit( float fAmount );
}

public class BusinessAccount : IAccount
{
    /* Data memebrs */
    float m_fBalance;

    /* Constructors */
    public BusinessAccount(float balance)
    {
        m_fBalance = balance;
    }

    /* IAccount implementation */
    public void OpenAccount( ICustomer customer)
    {
        // Get customer name
        string strName = customer.Name;

        // ...
    }

    public void CloseAccount()
    {
        // ...
    }

    public void Deposit( float fAmount )
    {
        // ...
    }
}

public class Customer : ICustomer
{
    /* Data memebrs */
    string m_strName;
    string m_strAddress;

    /* Constructors */
    public Customer(string name, string address)
    {
        m_strAddress = address;
        m_strName = name;
    }

    /* ICustomer implementation */
    public string Name
    {
        get { return m_strName; }
        set { m_strName = value; }
    }

    public string Address
    {
        get { return m_strAddress; }
        set { m_strAddress = value; }
    }
}

// Create a new customer and a new business account
Customer customer       = new Components.Customer( "MyName", "MyAddress" );
BusinessAccount account = new BusinessAccount((float)1000.00);

// Now call methods
account.OpenAccount( customer );
account.Deposit( (float)1000000.00 );

Displaying Forms from Components

In general, components do not have visual interfaces (Controls do). However, there may be times when the component needs to interact with the customer visually. For example, a component for managing bank accounts may wish to display a specialized acknowledgment dialog box when money is to be withdrawn. Such visual interaction is specific to the component functionality and hence needs to remain within the component.

Because components are classes, it is easy to create a form and display it from within your component. There are two approaches you can use to create a form:

The next step is to display the form. This is very easy:

public void Withdraw( double dAmount )
{
    //  Process request
    ...

    // Now display an acknowledgment
    FormAcknowldge frm = new FormAcknowldge();
    frm.ShowDialog();
    frm.Close();
}

Packaging Components

Code libraries are convenient way to create and reuse components. A code library is basically an assembly that compiles to a DLL (other assemblies may compile to .EXE). Code libraries provide a convenient way to encapsulate code in a single file, allow for inheritance and modification of these files, and permit distribution of discrete units of functionality. To create a code library:

Keep in mind that some languages may not support optional parameters. This can be usually taken care of by providing multiple overloads of the same method. Also keep in mind the access levels of your components. If these components are meant to be used from a client application, the components must be made public. If these components are meant to be used by other components within your package, then the components should be made internal.