Click here to Skip to main content
Click here to Skip to main content

Introducing Castle - Part I

By , 30 Dec 2004
Rate this:
Please Sign up or sign in to vote.

Introduction

The construction of a well-designed system is challenging, whatever its size, and the main challenges could be summarized as follows:

  • Objects' responsibilities
  • Extensibility
  • Reutilization

By objects' responsibilities I mean the ability of designing classes that have a clear, distinct responsibility. In other words, a class should have only one concern and the concern should be very explicit through its name and operations. It might sound trivial but any developer knows it's not. Unfortunately, this only comes with experience, and there is no shortcut.

Extensibility, however, is a polemic topic as XP enthusiasts, like myself, advocates that the extensibility exposed by an application should match the requirements brought by the client. Nothing more, nothing less.

Reuse is tricky. The more your classes depend on each other, the bigger is your headache to reuse them in different applications or different contexts. Usually, we talk in levels of linkage: is your system tight coupled or loosely coupled?

This is the first part of three articles intended to show how inversion of control might result in less code, simpler design and testable software. In this part, I'll mainly focus on what is a decoupled design and how an inversion of control container might help you. I'll also describe Castle's Windsor Container, how it works, and how to augment it.

Inversion of Control

What's inversion of control, anyway? Inversion of control is a simple principle that dictates that an external entity should send messages to programmer's objects in order to perform some specialized action or to allow the programmer to override some logic. This is the main difference between an API and a framework. On the first case, your object uses the API in an active fashion; in the second case, the framework uses your objects, so your objects are now in a passive mode.

Putting off all the hype about Inversion of Control and the wrong meaning some legends of software development try to impose to it, inversion of control is a nice design principle to construct a loosely coupled application. It requires that you shift your paradigm, though. So, before you embrace the principle, do some - or several - experimentations. It might be good to browse the code of some projects that use inversion of control too, like Cocoon, James and Turbine.

So, one of the uses of inversion of control that solves the loosely coupled problem (or one aspect of it) is by inverting how your class obtains external object references or configuration. Let's work on a not-so-distant-from-the-real-world example:

using System;
using System.Configuration;

public class MyAwfulEmailClass
{
  public MyAwfulEmailClass()
  {
  }
  
  public void SendEmail( String from, String to, String message, String templateName )
  {
    String host = ConfigurationSettings.AppSettings["smtphost"];
    int port = Convert.ToInt( ConfigurationSettings.AppSettings["smtpport"] );

    NVelocityTemplateEngine engine = new NVelocityTemplateEngine();
    String newMessage = engine.Process( message, templateName );

    // Finally send message...
  }
}

This class has a few lines of code, but has screaming problems:

  • It obtains configuration from the ConfigurationSettings. What if the configuration is not there? What if you'd like to use Yaml instead of XML to hold configuration?
  • It instantiates a template engine. In fact, there are two problems here: one is that this class has more responsibility than the name implies. It sends an email, that's for sure, but it also uses a template engine to process the message contents. The second problem is that the class has deep knowledge about the template engine it uses. If you'd like to change the engine later, you're going to dig code.

So this simple class has two strong dependencies: the means to obtain the configuration and the template engine. Suppose you'd like to use this very class in another project. In order to do that, you'll have to:

  • Remember about the configuration keys.
  • Bring the NVelocityTemplateEngine class and all the dependencies it might have, with you.

And this is just a small class! It could be worse, and I'm positive you have seen a similar situation. Usually, the fastest solution is to copy the code and change a few things. But c'mon, there must be another way. Fortunately, there is.

Component and Services

There were people using an interesting buzzword: COP (component oriented programming). I think we already have enough of buzzwords nowadays and we should save space on our brains for more important things. Nevertheless, I'd like to depict the concepts of components.

A component is a small unit of reusable code. It should implement and expose just one service, and do it well. In practical terms, a component is a class that implements a service (interface). The interface is the contract of the service, which creates an abstraction layer so you can replace the service implementation without effort.

To define our email service, we could use the following interface:

// The contract
public interface IEmailSender
{
  void Send(String from, String to, String message)
}

Please note that this contract is valid for any implementation of e-mail sender, for example, using SMTP, IMAP, SendMail and so on.

Now, we can work on one of those implementations, a simple SMTP e-mail sender:

// The implementation
public class SmtpEmailSender : IEmailSender
{
  private String _host;
  private int _port;

  public SmtpEmailSender(String host, int port)
  {
    _host = host;
    _port = port;
  }

  public void Send(String from, String to, String message)
  {
    // Configures the Smtp class and sends the e-mail
  }
}

Better, huh? But where's the inversion of control? Before going further, allow me to make things more complex. You have probably noticed that now the email sender is only responsible for sending the email, no template processing. So let's define the contract for a template processor engine:

public interface ITemplateEngine
{
  String Process(String templateName)
}

This is a fairly naive example. Any decent template engine will use a context or something similar. Now we can have a different component which processes the templates and dispatches the emails:

public interface INewsletterService
{
  void Dispatch(String from, String[] targets, String messageTypeName)
}

Let's speculate about the implementation of INewsletterService. It certainly needs to use the IEmailSender service and the ITemplateEngine service without even caring about their implementation. Fair enough, let's code:

public class SimpleNewsletterService : INewsletterService
{
  private IEmailSender _sender;
  private ITemplateEngine _templateEngine;
  
  public SimpleNewsletterService(IEmailSender sender, 
                      ITemplateEngine templateEngine)
  {
    _sender = sender;
    _templateEngine = templateEngine;
  }

  public void Dispatch(String from, String[] targets, String messageTypeName)
  {
    String message = _templateEngine.Process(messageTypeName);

    foreach(String target in targets)
    {
      _sender.Send(from, target, message);
    }
  }
}

As you can see, with small and well defined responsibilities, it's easier to cope with software development. But the problem now is that we have to assemble the application, in other words, we need to connect everything, instantiating and configuring the IEmailSender and the ITemplateEngine, pass them to INewsletterService and God knows what else. You can do it by hand, but believe me, things can get really complicated with a big system.

Castle Windsor

Castle Windsor is an inversion of control container. It's built on top of a MicroKernel responsible for inspecting the classes and trying to understand what they need in order to work and then provide the requirements or fail fast if something is wrong.

For our small example, the following code will take care of everything:

IWindsorContainer container = new WindsorContainer();
container.AddComponent( "newsletter", typeof(INewsletterService), 
                        typeof(SimpleNewsletterService) );
container.AddComponent( "smtpemailsender", typeof(IEmailSender), 
                        typeof(SmtpEmailSender) );
container.AddComponent( "templateengine", typeof(ITemplateEngine), 
                        typeof(NVelocityTemplateEngine) );

// Ok, start the show
INewsletterService service = (INewsletterService) container["newsletter"];
service.Dispatch("hammett at gmail dot com", friendsList, "merryxmas");

First, let me explain what happened in this code snippet:

  1. You registered the service INewsletterService on the container and you also said that the class SimpleNewsletterService contains an implementation of INewsletterService. The first argument is a key that you use to request this service later. You might also request the service by the Type.
  2. Windsor inspected the implementation you provided and noticed that it requires two other services to work. It checked itself and realized that it doesn't have such services registered, so if somebody requests the INewsletterService service instance, it will throw an exception.
  3. You registered the service IEmailSender. Windsor notices that the implementation has just one public constructor that receives two arguments (host and port). It's not able to satisfy this configuration as we haven't provided an external configuration. We'll fix that later.
  4. You registered the service ITemplateEngine. Windsor registers it successfully and shouts to other components in a WaitingDependency state that a component has been registered. The INewsletterService realizes that one of its dependencies is OK to go, but it still is waiting for the other.

The fact that a class has a non default constructor has a specific meaning for the container. The constructor says to it: "Look, I really need these in order to work". If our implementation were different, the container would have used a different approach. For example:

public class SmtpEmailSender : IEmailSender
{
  private String _host;
  private int _port;

  public SmtpEmailSender()
  {
    _host = "mydefaulthost";
    _port = 110; // default port
  }

  public String Host
  {
    get { return _host; }
    set { _host = value; }
  }
  
  public int Port
  {
    get { return _port; }
    set { _port = value; }
  }

  ...
}

In this case, you may or may not specify the host and port in an external configuration and the container will be able to use it. Another approach is to expose more than one constructor. The container will try to use the best constructor - meaning the one it can satisfy more arguments.

"But wait! What about the configuration?"

By default, Windsor will try to obtain the component configuration from the XML file associated with the AppDomain. However, you can override it by implementing the interface IConfigurationStore. There's another implementation of IConfigurationStore that reads the configuration from standard XML files, which is good when you have a configuration for testing environment, another for production, and so on.

Here is the configuration file you must use to run the example:

<?xml version="1.0" encoding="utf-8" ?> 

<configuration>

    <configSections>
        <section name="castle"
          type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, 
                Castle.Windsor" />
    </configSections>

    <castle>
        <components>
            <component id="smtpemailsender">
                <parameters>
                    <host>localhost</host>
                    <port>110</port>
                </parameters>
            </component>
        </components>
    </castle>

</configuration>

From experience, I know that sometimes it's dumb to have interfaces for all your components. There are classes that are unlikely to have a different implementation, like Data Access Objects. In this case, you can register classes into the container as follows:

public class MyComponent
{
  private IEmailSender _sender;

  public MyComponent()
  {
  }
  
  public IEmailSender EmailSender
  {
    get { return _sender; } 
    set { _sender = value; } 
  }
}

IWindsorContainer container = new WindsorContainer();
container.AddComponent( "mycomponent", typeof(MyComponent) );

You might want to request a component by the service:

IEmailSender emailSender = container[ typeof(IEmailSender) ] as IEmailSender;

But please note that you can register more than one implementation for a given service. The default behavior is to return the first implementation registered for the specified service. If you want a specific implementation, then you need to use the key.

As a matter of fact, the last paragraph leads us to an interesting situation. Suppose you have implemented several IEmailSender but you want that the INewsletterService implementation uses the SMTP version and nothing else. The better way to do it is to use the configuration to specify the exact implementation you want:

<?xml version="1.0" encoding="utf-8" ?> 

<configuration>

    <configSections>
        <section name="castle"
             type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, 
                   Castle.Windsor" />
    </configSections>

    <castle>
        <components>
            <component id="newsletter">
                <parameters>
                    <sender>#{smtpEmailSender}</sender>
                </parameters>
            </component>
        </components>
    </castle>

</configuration>

The nodes inside 'parameters' must use the name of a property exposed by the implementation or the name of the argument the implementation's constructor uses. Just to refresh your memory:

public class SimpleNewsletterService : INewsletterService
{
  private IEmailSender _sender;
  private ITemplateEngine _templateEngine;
  
  public SimpleNewsletterService(IEmailSender sender, ITemplateEngine templateEngine)
  {
    _sender = sender;
    _templateEngine = templateEngine;
  }
  
  ...

Lifecycle and Lifestyle

Lifecycle and lifestyle are useful strategies that Apache Avalon have been using for quite some time (since 1999 to be exact). As a former member of Avalon, I like these ideas and decided to provide it too.

The lifestyle of a component is related to its instance. It might be singleton (which is the default lifestyle) that means that only one instance will be created for the whole life of the container. Other supported lifestyles are:

  • Transient: a new instance is created per request.
  • PerThread: just one instance exists per thread.
  • Custom: you provide the implementation of ILifestyleManager to implement your own semantics, like poolable objects.

There are two ways to specify the lifestyle of a component: by using attributes or by using the external configuration. In fact, the configuration overrides whatever is defined on the component. The following have the same result:

using Castle.Model;

[PerThread]
public class SimpleNewsletterService : INewsletterService
{
  private IEmailSender _sender;
  private ITemplateEngine _templateEngine;
  ...
<castle>
  <components>
        <component id="newsletter" lifestyle="perthread" />
    </components>
</castle>

Lifecycle means the sequence of invocation that happens during the creation of your component (before making it available to the external world) and the sequence of invocation during the destruction. Out of box, Windsor only supports the IInitialize and the standard IDisposable interfaces.

More about Windsor Container

When you add a component to the container, a few things happen. First, the container creates a ComponentModel which holds a lot of meta information about the component. Then it delegates the inspection of the component to the contributors. Each contributor cares about one specific task. For example, there is a contributor that gathers all public constructors and adds to the ComponentModel, another one checks if the implementation implements specific lifecycle interfaces, another one looks for specific attributes of lifestyle, and so on.

<!--

-->

The real fun begins when you start to use this process by adding your own contributor that checks for _anything_ and gives the component a different semantic or role when found that _anything_.

There's also an important entity called Handler. The Handler is the keeper and orchestrator of the component. If the minimal set of dependencies for the component is not satisfied, the handler will not allow the component to be used (by your code or by any other component).

Now, you're ready for the most interesting section: extending the container.

Extending the Container

I'm going to present you a simple and a not so simple sample applications. The first one will introduce a different semantic to components that implement the IStartable interface; the other depicts a simple yet functional transaction infrastructure for your Data Access Objects and two ways of using it.

Basically, to extend the container, you can use a few extension points as you like. For example, you can add an inspector to look for an attribute, or an entry on the component configuration, or whatever you like. You can also subscribe to the events the MicroKernel exposes. You can even replace some parts of the MicroKernel and implement different semantics for your container (for example, to allow null values be passed to components' constructors).

When extending the container, you can give the component instance more information, register interceptors (proxies), change the lifestyle, add more lifecycles and so on. At first, let's talk about implementing a new lifecycle step.

Startable Lifecycle

Suppose you'd like that every component that implements an IStartable interface be started as soon as possible. By started, I mean:

  • Create an instance of it by requesting it.
  • Invoke the Start method defined by IStartable, so the component may start its work.

For a world usage example, imagine a WinForms application so you can register Forms subclasses as components. One component may run the Application.Run( your main form ). Thus, this component is likely to benefit from the new lifecycle. We'll use the components developed previously just to pretend we have some real action going on.

When you decide to implement extensions like that, you can do it ad-hoc, or you can create a Facility. Facility is an extension unit so you can create a library of facilities and apply them to other applications, thus reusing your work.

To implement our requirements, we need to:

  1. Hook into the component registration process so we can inspect if the component being registered implements the IStartable interface.
  2. Add a new lifecycle step that executes the Start method accordingly.

So, let's work! The first step: create a new Facility:

public class StartableFacility : IFacility
{
    private IKernel _kernel;

    public void Init(IKernel kernel, IConfiguration facilityConfig)
    {
        _kernel = kernel;
    }

    public void Terminate()
    {
        // Nothing to do
    }
}

A Facility can have its own configuration which can be useful for settings (like the NHibernate facility that I'll talk about in the next article).

Now, let's add a hook to the registration process:

public class StartableFacility : IFacility
{
    private IKernel _kernel;

    public void Init(IKernel kernel, IConfiguration facilityConfig)
    {
        _kernel = kernel;
        kernel.ComponentModelBuilder.AddContributor( new StartableInspector() );
    }

    public void Terminate()
    {
        // Nothing to do
    }

    private class StartableInspector : IContributeComponentModelConstruction
    {
        public void ProcessModel(IKernel kernel, ComponentModel model)
        {
            bool startable = 
                typeof(IStartable).IsAssignableFrom(model.Implementation);

            model.ExtendedProperties["startable"] = startable;

            if (startable)
            {
                model.LifecycleSteps.Add( 
                    LifecycleStepType.Commission, new StartableConcern() );
            }
        }

        private class StartableConcern : ILifecycleConcern
        {
            public void Apply(ComponentModel model, object component)
            {
                (component as IStartable).Start();
            }
        }
    }
}

So we added a contributor that looks for the IStartable interface. If found, we add a new lifecycle step which implements the ILifecycle interface. Simple, huh? But we still need to implement the logic to start the components as soon as possible. Here we go:

public class StartableFacility : IFacility
{
    private ArrayList _waitList = new ArrayList();
    private IKernel _kernel;

    public void Init(IKernel kernel, IConfiguration facilityConfig)
    {
        _kernel = kernel;

        kernel.ComponentModelBuilder.AddContributor( new StartableInspector() );
        kernel.ComponentRegistered += 
            new ComponentDataDelegate(OnComponentRegistered);
    }

    ...

    private void OnComponentRegistered(String key, IHandler handler)
    {
        bool startable = (bool) 
            handler.ComponentModel.ExtendedProperties["startable"];

        if (startable)
        {
            if (handler.CurrentState == HandlerState.WaitingDependency)
            {
                _waitList.Add( handler );
            }
            else
            {
                Start( key );
            }
        }

        CheckWaitingList();
    }

    /// For each new component registered,
    /// some components in the WaitingDependency
    /// state may have became valid, so we check them
    private void CheckWaitingList()
    {
        IHandler[] handlers = (IHandler[]) 
            _waitList.ToArray( typeof(IHandler) );

        foreach(IHandler handler in handlers)
        {
            if (handler.CurrentState == HandlerState.Valid)
            {
                Start( handler.ComponentModel.Name );

                _waitList.Remove(handler);
            }
        }
    }

    /// Request the component instance
    private void Start(String key)
    {
        object instance = _kernel[key];
    }
}

Now, if the component that was just registered is startable, we try to start it by requesting it. Note however that sometimes the components are not ready to be started, so we have to keep them on a list and check if they are OK to be started later.

If you'd like to see the application running, check the sample code.

A Transaction Framework for database access

In this example, the goal is to simplify the development of Data Access Objects by automatic handling of the connection and transactions. We purse two possible usages:

Through attributes:

[Transactional]
public class BlogDao
{
    private IConnectionFactory _connFactory;

    public BlogDao(IConnectionFactory connFactory)
    {
        _connFactory = connFactory;
    }

    [RequiresTransaction]
    public virtual Blog Create(Blog blog)
    {
        using(IDbConnection conn = _connFactory.CreateConnection())
        {
            IDbCommand command = conn.CreateCommand();

            // Not the best way, but the simplest
            command.CommandText = 
                String.Format("INSERT INTO blog (name, blog.desc) " + 
                "values ('{0}', '{1}');select @@identity", 
                blog.Name, blog.Description);

            object result = command.ExecuteScalar();
            blog.Id = Convert.ToInt32(result);
        }

        return blog;
    }

    [RequiresTransaction]
    public virtual void Delete(String name)
    {
        // We pretend to delete the blog here
    }

    ...
}

And through external configuration:

<component id="postDao" transactional="true">
  <transaction>
    <method>Create</method>
    <method>Update</method>
  </transaction>
</component>

Might sound complex, but it's not. We need to implement an IConnectionFactory which is aware of transactions. We also need to implement an interceptor that begins/commits or rolls-back transactions.

It all starts with a Facility again:

public class TransactionFacility : IFacility
{
    TransactionConfigHolder _transactionConfigHolder;

    public void Init(IKernel kernel, IConfiguration facilityConfig)
    {
        kernel.AddComponent( "transactionmanager", 
            typeof(ITransactionManager), typeof(DefaultTransactionManager) );
        kernel.AddComponent( "transaction.interceptor", 
            typeof(TransactionInterceptor) );
        kernel.AddComponent( "transaction.configholder", 
            typeof(TransactionConfigHolder) );

        _transactionConfigHolder = 
            kernel[ typeof(TransactionConfigHolder) ] 
            as TransactionConfigHolder;

        kernel.ComponentModelCreated += new 
               ComponentModelDelegate(OnModelCreated);
    }

    public void Terminate()
    {
    }

    private void OnModelCreated(ComponentModel model)
    {
        if (IsTransactional(model))
        {
            TransactionConfig config = CreateTransactionConfig(model);

            _transactionConfigHolder.Register(model.Implementation, config);

            model.Interceptors.Add( 
                new InterceptorReference(typeof(TransactionInterceptor)) );
        }
    }

    ...
}

This Facility registers the components it needs. If the component is transactional, we associate an interceptor with it. Please note that only virtual methods can be intercepted (in future releases, we'd be able to proxy everything through interfaces).

The interceptor code follows:

public class TransactionInterceptor : IMethodInterceptor
{
    private ITransactionManager _transactionManager;
    private TransactionConfigHolder _transactionConfHolder;

    public TransactionInterceptor(ITransactionManager transactionManager, 
        TransactionConfigHolder transactionConfHolder)
    {
        _transactionManager = transactionManager;
        _transactionConfHolder = transactionConfHolder;
    }

    public object Intercept(IMethodInvocation invocation, params object[] args)
    {
        if (_transactionManager.CurrentTransaction != null)
        {
            // No support for nested transactions
            // is necessary
            return invocation.Proceed(args);
        }

        TransactionConfig config = 
            _transactionConfHolder.GetConfig( 
                invocation.Method.DeclaringType );

        if (config != null && config.IsMethodTransactional( invocation.Method ))
        {
            ITransaction transaction = 
                _transactionManager.CreateTransaction();

            object value = null;

            try
            {
                value = invocation.Proceed(args);

                transaction.Commit();
            }
            catch(Exception ex)
            {
                transaction.Rollback();
                throw ex;
            }
            finally
            {
                _transactionManager.Release(transaction);
            }

            return value;
        }
        else
        {
            return invocation.Proceed(args);
        }
    }
}

The rest of the implementation is pretty ADO.NET specific, and I encourage you to see the sample attached to this article. Follows some screenshots of the sample implementation:

Stepping back: Castle

Castle project's goal is to offer a set of tools and an inversion of control container. The container is the rendezvous of all the tools, but this is a topic for the next article.

A few values have driven the development of the Windsor (and MicroKernel) container:

  • Extensibility: the container should not stop the programmer from implementing his ideas, so it must be very easy to extend it.
  • Orthogonality: extending the container should not impact on other extensions (as you saw).
  • Reversibility: the container should be able to run code that does not rely on the container API, so you can reuse your components even on other applications without using an inversion of control container.

We also don't want to be XML or configuration driven as the component holds enough information to assemble itself.

Conclusion

In the next article, I'll talk about the NHibernate and the Prevalence facilities. I'll also explain the integration with Web and the Castle on Rails MVC framework inspired by Ruby on Rails.

The Castle project still is on the alpha stage, but the public API is very unlikely to change. There is a lot of work to do, so if you enjoyed the reading, consider joining the team and help us develop tools, facilities and extensions so your next enterprise project can be done in half the time, hopefully!

Please visit Castle project site for more information.

History

  • 26-Dec-2004 - Initial version.
  • 29-Dec-2004 - Minor corrections on the text and on the sample.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

About the Author

Hamilton Verissimo
Web Developer
Brazil Brazil
Hamilton Verissimo has been working with software development for 9 years. He's involved with a lot of open source projects to fill his spare time.

Comments and Discussions

 
Questioncongratulation and aknowledgment Pinmembersnowymans@gmail.com7-Mar-14 10:12 
QuestionAddComponent method not exists in Castle.Windsor 3.1.0.0 PinmemberKasie28-Sep-12 2:48 
AnswerRe: AddComponent method not exists in Castle.Windsor 3.1.0.0 Pinmemberandresfalcon10-Feb-13 4:24 
GeneralMy vote of 5 Pinmemberbobonlinetsl28-Jun-12 1:35 
GeneralThanks - Pinmembernoofny9-Mar-10 17:21 
GeneralVery nice article PinmemberVerifier31-Jul-07 6:03 
GeneralProblem with sample1 Pinmembermarwelous28-Sep-05 5:02 
AnswerRe: Problem with sample1 PinmemberHamilton Verissimo29-Sep-05 15:07 
GeneralRe: Problem with sample1 Pinmembermarwelous29-Sep-05 20:19 
Hi,
 
thanks for answer. Reading the Windsor source code I supposed this answer.
 
But anyway, what about to make such a nice article up to date. I think it is not good advertisement of great Castle project when the simplest example does not work. Wink | ;)
 
Ondrej
 

GeneralRe: Problem with sample1 PinmemberMaherG17-Jun-06 18:57 
GeneralA small problem with instantiation Pinmembercklein13-Apr-05 11:55 
GeneralRe: A small problem with instantiation Pinmembercklein13-Apr-05 12:13 
GeneralRe: A small problem with instantiation PinmemberHamilton Verissimo13-Apr-05 12:41 
GeneralBest article I've ever read Pinmembercklein13-Apr-05 6:11 
GeneralRe: Best article I've ever read PinmemberHamilton Verissimo13-Apr-05 7:14 
GeneralSmall error PinsussAnonymous18-Mar-05 2:22 
GeneralRe: Small error PinmemberHamilton Verissimo18-Mar-05 2:40 
GeneralSimilar project Pinmembernicpaez19-Jan-05 7:08 
GeneralRe: Similar project PinmemberHamilton Verissimo19-Jan-05 7:53 
GeneralCom + PinmemberReuven Kadison5-Jan-05 20:43 
GeneralRe: Com + PinmemberHamilton Verissimo6-Jan-05 8:05 
GeneralAdditional reading PinmemberErnesto Perales Soto3-Jan-05 7:01 
GeneralRe: Additional reading PinmemberHamilton Verissimo3-Jan-05 7:36 
GeneralRe: Additional reading PinmemberPugster5-Nov-05 7:35 
GeneralReally good! PinmemberPeter Hancock28-Dec-04 13:27 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140415.2 | Last Updated 30 Dec 2004
Article Copyright 2004 by Hamilton Verissimo
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid