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. |
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.
In general, make a constructor public to allow users to create and use your control.
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
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 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;
}
}
}
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:
- Features can be added to the component incrementally by defining and implementing additional interfaces.
- A component that implements an interface can provide services to another component without regard to particular functionality contained in the component implementing the interface.
- Design effort is simplified because components can begin small with minimal functionality and then grow appropriately.
- Maintaining compatibility is simplified because new versions of a component can continue to provide existing interfaces while adding new one.
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()");
}
}
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:
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:
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 );
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();
}
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.