Click here to Skip to main content
12,698,410 members (24,027 online)
Click here to Skip to main content
Add your own
alternative version

Stats

16.7K views
1.1K downloads
56 bookmarked
Posted

Introduction to Dependency Injection and Inversion of Control

, 26 Feb 2016 CPOL
Rate this:
Please Sign up or sign in to vote.
A gentle introduction for DI and IOC in the first program most of us have written - Hello World

Introduction

What is the most iconic program ever written? In my mind it’s the programmers first program “Hello Word” it’s where you see the very first fruits of your efforts. The first time you convert a thought to an action in the programming world.


In this article I have made this classic programming example way too complicated. Converting what should be one file in one project in one solution to five separate examples with the very first example containing five separate projects in one solution.


These examples are not intended to be examples of writing “Hello World” in a complex way; but rather in a way which can help explain dependency injection and inversion of control in a way which is not clouded by other technologies. What better way to do that then to make the core of the program do only one thing and that is to display “Hello World”


There are a number of dependency injection frameworks available. These examples use Structuremap because it is one of the oldest frameworks and the framework of choice as these example programs where written. 

Background

Originally these were put together for examples to explain these principals to other developers with whom I worked, as part of this update I also moved from structuremap 2.x to version 4.x

What is Dependency Injection

A dependency is an object that can be used (a service). An injection is the passing of a dependency to a dependent object (a client) that would use it. The service is made part of the client's state. Passing the service to the client, rather than allowing a client to build or find the service, is the fundamental requirement of the pattern. 

source - https://en.wikipedia.org/wiki/Dependency_injection

As an example:

static void Main(string[] args)
{
      //Instead of service being created in Client, it is injected through the constructor
      service sc = new service();
      Client cc = new Client(sc);
       
      //Do work. 
      cc.DoSomeThing();
}

Although a dependency can be passed to an object as a concrete class, it is more generally accepted to pass an object which implements an interface. Coding against the contract (Interface) allows you to pass any concrete class which implements the contract.

Example:

  •     Public class SQLDBConnection : IDBConnection
  •     Public class ODBCConnection : IDBConnection
  •     Public class OracleConnection : IDBConnection

All classes share the same IDBConnection contract definition.

  •     Programming against the interface allows you not to worry about implementation
  •     Factories can be used to create the concrete class
  •     IDBConnection = DBFactory(“configfile”)

What is Inversion of Control?

In traditional programming, the flow of the business logic is determined by objects that are statically bound to one another. With inversion of control, the flow depends on the object graph that is built up during program execution. Such a dynamic flow is made possible by object interactions being defined through abstractions. This run-time binding is achieved by mechanisms such as dependency injection or a service locator. In IoC, the code could also be linked statically during compilation, but finding the code to execute by reading its description from external configuration instead of with a direct reference in the code itself.

In dependency injection, a dependent object or module is coupled to the object it needs at run time. Which particular object will satisfy the dependency during program execution typically cannot be known at compile time using static analysis. 

source - https://en.wikipedia.org/wiki/Inversion_of_control 

static void Main(string[] args)
{
     //Instead of service being created in Client, it is injected through the constructor
     //by the IoC Container
     using (var sm = new Injector.ContainerBootstrapper())
     {
          //Using an Interface to code against
          IClient cc = sm.GetInstance<IClient>();

          //Do work. 
          cc.DoSomeThing();
     }
}

How are concrete classes injected?

  •     Factory Pattern
  •     Service Locator pattern
  •     Dependency injection
    •     Constructor injection
    •     Parameter injection
    •     Setter injection
    •     Interface injection
  •     Other patterns

You will see examples of both the Service Locator pattern as well as Dependency injection as part of this article.

Structuremap

Structuremap is a Dependency Injection / Inversion of control tool for .Net that can be used to improve the architectural qualities of an object oriented system by reducing the mechanical cost of good design techniques

http://docs.structuremap.net/ - Version 2.X
http://structuremap.github.io/ - Version 4.X

 Consider using StructureMap if you:

Require significant extensibility
Simply want a generic configuration tool
Want to support multiple deployment configurations
Are using a Test-Driven Development philosophy or want to largely automate testing
Want to isolate a troublesome subsystem or provide smooth migration paths away from legacy interfaces
Need a great deal of configurable properties or plugin hot spots 

Objects created by structuremap can be configured for the following life times:

  • PerRequest - The default operation.  A new instance will be created for each request.
  • Singleton - A single instance will be shared across all requests
  • ThreadLocal - A single instance will be created for each requesting thread. Caches the instances with ThreadLocalStorage.
  • HttpContext - A single instance will be created for each HttpContext. Caches the instances in the HttpContext.Items collection.
  • HttpSession - A single instance will be created for each HttpSession. Caches the instances in the HttpContext.Session collection.  Use with caution.
  • Hybrid - Uses HttpContext storage if it exists, otherwise uses ThreadLocal storage.

Other IOC options – Partial Listing

Microsoft Unity
Ninject
Castle Windsor
Spring.Net
Autofac

Using the code

A_IOC – Our starting point. Still more complex than simply writing:

static void Main(string[] args)
{
     Console.Write("Hello World");
     Console.Read();
}

This project set the stage, all of the other solutions will build from this one. As this article progresses you will be directed to points of interest in each solution to compare and contrast.

All the projects contain the following projects:

  • IOC - Main
     static void Main(string[] args)
     {
        LoggerClass.Logger l = 
                 new LoggerClass.Logger(LoggerClass.Logger.LogLevel.Info,@"\logA","logfile");
        FacadeClass.FacadeClass f = new FacadeClass.FacadeClass();

        l.Log("Start", "Main", LoggerClass.Logger.LogLevel.Info);
        Console.Write(f.StringHelloWorld());
        l.Log("End", "Main", LoggerClass.Logger.LogLevel.Info);
            
        Console.Read();
     }
  • FacadeClass - Uses ObjectHello and ObjectWorld to create the displayed string.
public class FacadeClass
 {
     ObjectHello.HelloClass a = new ObjectHello.HelloClass();
     ObjectWorld.WorldClass b = new ObjectWorld.WorldClass();

     public FacadeClass()
     {
         a.CreateNewHello("Hello");
         b.CreateNewWorld("World");
     }

     public string StringHelloWorld()
     {
         return a.Hello + ' ' + b.World;
     }
 }
  • ObjectHello and ObjectWorld - Contrived classes returning what is passed in on the contructer.
public class HelloClass
{
    public void CreateNewHello(string hello)
    {
        Hello = hello;
    }
    public string Hello { get; set; }
}
  • LoggerClass - Simple logger
  • Later on, we will add two additional projects called InterfaceClass and Injector

If we were to look at how intertwined each piece was together it look like this.

 

B_IOC – The jouney starts:

Firstly, notice this solution contains a new project called InterfaceClass. This is a project which will eventually become the focal point of this and all solutions to follow.

It is simply were we keep all our interfaces for the solution. Remember an interface is a contract which objects that implement the interface have to provide a concrete implementation. The interface itself contains no implementation code, only signatures to program against.

public interface IFacadeClass
{
    //any class that implements this must satisfy this contract
    string DisplayStuff();
}
 public class FacadeClass : Interfaces.IFacadeClass
 {

     Interfaces.IHelloClass a = new ObjectHello.HelloClass();
     Interfaces.IWorldClass b = new ObjectWorld.WorldClass();

     public FacadeClass()
     {
         a.CreateNewHello("Hello");
         b.CreateNewWorld("World");
     }

     //implementation of interface
     public string DisplayStuff()
     {
         return a.Hello + ' ' + b.World;
     }
}

Secondly, notice all the project files now derive from the Interfaces.IFacadeClass and must implement their respective contracts.

Thirdly, notice local variables have been replaced with their Interface counterpart.

Instead of:

    LoggerClass.Logger l = new LoggerClass.Logger(LoggerClass.Logger.LogLevel.Info,@"\logA","logfile");
    FacadeClass.FacadeClass f = new FacadeClass.FacadeClass();
        
    ObjectHello.HelloClass a = new ObjectHello.HelloClass();
    ObjectWorld.WorldClass b = new ObjectWorld.WorldClass();

We have now:

    ILogger l = new LoggerClass.Logger(Interfaces.LogLevel.Info, @"\logB", "logfile");
    Interfaces.IFacadeClass f = new FacadeClass.FacadeClass();
        
    Interfaces.IHelloClass a = new ObjectHello.HelloClass();
    Interfaces.IWorldClass b = new ObjectWorld.WorldClass();

The codemap now looks like this:

 

C_IOC – Ready to inject the dependencies:

In this project, the only thing that really changes is the facade. We change this to not rely on the objects being instantiated in the facade itself. As written, this project will compile but will fail when executing. The method StringHelloWorld will fail with a null reference error.

Additionally we are passing in a reference to the log object to log inside the facade class.

Comments are supplied to correct and run successfully.

    public class FacadeClass : Interfaces.IFacadeClass
    {

        Interfaces.IHelloClass a;
        Interfaces.IWorldClass b;

        public FacadeClass() { }

        //We can't call this directly since we have no reference to an ObjectHello or 
        //ObjectWorld in Program.cs .
        //We can add the reference and call this method. See Program.cs commented out section.
        public FacadeClass(Interfaces.IHelloClass ac, Interfaces.IWorldClass bc,Interfaces.ILogger lg)
        {
            a = ac;
            b = bc;
            //before using, make sure we have a reference.
            if (lg != null)
                //Since we have 
                lg.Log("Start", "Facade",Interfaces.LogLevel.Error);

            a.CreateNewHello("Hello");
            b.CreateNewWorld("World");
        }

        
        public string StringHelloWorld()
        {
            //Although this compiles, it does not run because we do not initialize variables 
            //a and b with instantiated objects
            //We are calling with base contructor and nothing gets instantiated
            //See Program.cs for possible correction. But this is desired effect for demo.
            return a.Hello + ' ' + b.World;
        }

    }

Codemap non-working:

Notice ObjectA and B (Hello and World) are created but nothing depends on them.

Now the objects are included.

static void Main(string[] args)
{

  //extranious code removed.
  
  //This doesn't run
  Interfaces.IFacadeClass f = new FacadeClass.FacadeClass();

  // Uncomment this and include Object Hello and Object World in referrences and it should work
  //Interfaces.IFacadeClass f = new FacadeClass.FacadeClass(new ObjectHello.HelloClass(), new                  ObjectWorld.WorldClass(), log);

}

D_IOC – Include structuremap:

In this project we include a new project called injector, it has a class called ContainerBootStrapper which will be responsible for wiring up the concrete classes to inject into the classes constructors.

You should Enable NuGet package restore to ensure that you automatically get structuremap assemblies. 

The projects which change include:

IOC - using container to return concrete object for logger and facade.

static void Main(string[] args)
{
    var sm = new Injector.ContainerBootstrapper();

    //Using the injector as a Service locator to return an instance of the logger and facade
    Interfaces.ILogger log = sm.GetInstance<Interfaces.ILogger>();       
    Interfaces.IFacadeClass f = sm.GetInstance<Interfaces.IFacadeClass>(); 

    if (log != null && f != null)
    {
        log.Log("Start", "Main", Interfaces.LogLevel.Warning);
        Console.Write(f.StringHelloWorld());
        log.Log("End", "Main", Interfaces.LogLevel.Warning);
    }
    Console.Read();
}

ObjectHello - Now logs as well and has the logger injected as part of the constructor.

public class HelloClass : Interfaces.IHelloClass
{
   Interfaces.ILogger _lg;

   //You will notice we do not pass in an instance, we allow the Resolver to fix this.
   //ObjectWorld does not log, so it has just the default ctor.
   public HelloClass(Interfaces.ILogger lg)
   {
       _lg = lg;
   }

   string _hello = string.Empty;

   public void CreateNewHello(string hello)
   {
       if(_lg != null)
          _lg.Log("Create Hello","HelloClass",Interfaces.LogLevel.Info);
        _hello = hello;
        
   }

   public string Hello { get { return _hello; } }
}

Injector - this is a class which should be instaniated as close to the begining of the program start as possible. You can see it is called in the very first line of Program.cs

public class ContainerBootstrapper
{

    Container _c;

    public ContainerBootstrapper()
    {
            // Initialize a new container
            _c = new Container(x =>
            {
                x.For<Interfaces.IHelloClass>().Use<ObjectHello.HelloClass>();
                x.For<Interfaces.IWorldClass>().Use<ObjectWorld.WorldClass>();
                x.For<Interfaces.IFacadeClass>().Use<FacadeClass.FacadeClass>();
                //The class lifetime can change. Now instantiated once.
                x.For<Interfaces.ILogger>().Singleton().Use<LoggerClass.Logger>()
                    .Ctor<Interfaces.LogLevel>("levelset").Is(Interfaces.LogLevel.Info)
                    .Ctor<String>("pathname").Is(@"\logD")
                    .Ctor<String>("filename").Is("logfile");

            });
    }

    public T GetInstance<T>()
    {
       return _c.TryGetInstance<T>();  
    }
     
}

The first three lines which are of format x.For<interface>().Use<concrete>() map the concrete class to us for the interface.

The fourth line is similar but changes it's lifetime to singleton and then sets the parameters to pass into the concrete class Logger. 

In earlier versions we would create a logger such as this:

Interfaces.ILogger log = new LoggerClass.Logger(Interfaces.LogLevel.Info, @"\logC", "logfile");

Now it is created by the container:

Interfaces.ILogger log = sm.GetInstance<Interfaces.ILogger>(); 
//level, location and name are set in the container creation.        

The codemap now lookes like this:

Notice now that since injector still needs a reference to each object to resolve, all the project DLLs will still be imported automatically into the application bin directory. 

E_IOC - Almost done!

In this version we break the dependency of including each object in the container to resolve. Additionally I have an example of AOP (Aspect Oriented Programming) where the real concrete class is wrapped with a similar class as a proxy and added functionality for centralized error handling.

IOC - added the IDisposable contract (interface) to the container so that we can wrap instantiation in a using.

using (var sm = new Injector.ContainerBootstrapper())
{ ... }

Injector - changed from fluent style of associating interface to concrete to scanning assemblies.

public class ContainerBootstrapper : IDisposable
{
    Container _c;
    public ContainerBootstrapper()
    {
         // Initialize the static ObjectFactory container
         _c = new Container(x =>
         {
             x.Scan(s =>
             {
                   
               s.AssembliesFromApplicationBaseDirectory(assm => assm.FullName.StartsWith("Object"));
               s.AssembliesFromApplicationBaseDirectory(assm => assm.FullName.StartsWith("Facade"));
               s.AssembliesFromApplicationBaseDirectory(assm => assm.FullName.StartsWith("Logger"));
               //This will use default assumption of object = IObject for mapping.
               s.WithDefaultConventions();
                     
             });
         });
         //The class lifetime can change. Now instantiated once.
         _c.Configure(a => a.ForSingletonOf<Interfaces.ILogger>());
         //Wrap the facade class with a proxy.
         _c.Configure(a => a.For<Interfaces.IFacadeClass>().DecorateAllWith<facadeAOP>());
       }
 
       public T GetInstance<T>()
       {
          return _c.TryGetInstance<T>();
       }

       public void Dispose()
       {
           _c.Dispose();
       }
   }
   
}

Notice a new class is added to the project called facadeAOP, this is a class which implements the same Interface the object which is resolved to the interface. DecorateAllWith will use this new class as a proxy. The proxy will instantiate the concrete class calling it when needed as well as the logger we need to log errors.

public class facadeAOP : Interfaces.IFacadeClass
{
   Interfaces.IFacadeClass ic; 
   Interfaces.ILogger _log

   //These will automatically get resolved
   public facadeAOP(Interfaces.IFacadeClass fc,Interfaces.ILogger log)
   {
      ic = fc;
      _log = log;
   }

   public string StringHelloWorld()
   {
       try
       {
          return ic.StringHelloWorld();
       }
       catch (Exception ex)
       {
          _log.Log(ex.Message,"AOP",Interfaces.LogLevel.Error);
       }

   }
}

LoggerClass - changed how it reads its configuration. In previous examples we were able to pass parameters to the constructor. I struggled with this version passing the parameters as I did in the D_IOC example but after thinking about it, realized that the best class for determining what parameters it needed from the config file was LoggerClass itself.

public class Logger : ILogger
{

   Interfaces.LogLevel _levelset;
   string _Pathname;
   string _Filename;
        
   public Logger()
   {
      var logSetting = ConfigurationManager.AppSettings;

     _levelset = (Interfaces.LogLevel)Enum.Parse(typeof(Interfaces.LogLevel), 
          logSetting["levelset"],true);
     _Pathname = logSetting["pathname"];
     _Filename = logSetting["filename"];
        
   }

   public void Log(string Message, string module, Interfaces.LogLevel level)
   { ... }

}

Codemap for this project shows how the objects are no longer a dependency for this project to build and the object can easily be swapped in and out as long as they implement the interface.

Caveats:

1. Since the projects are no longer referenced by the core program (only interface and injector are) they do not automatically move to the application bin directory. This needs to be done for each project built.

I would change to post build event to always on each project.

You need to set the build order for IOC so that all projects are built first.

 

A new addition

E_IOC - Ninject for all you code Ninjas!

I have added a new project to this article which shows the use of Ninject rather than Structuremap IOC. The changes are made to only the injector class. No other changes have been made.

In structure map, the configuration was done this way:

_c = new Container(x =>
     {

         x.Scan(s =>
         {
              s.AssembliesFromApplicationBaseDirectory(assm => assm.FullName.StartsWith("Object"));
              s.AssembliesFromApplicationBaseDirectory(assm => assm.FullName.StartsWith("Facade"));
              s.AssembliesFromApplicationBaseDirectory(assm => assm.FullName.StartsWith("Logger"));
              s.WithDefaultConventions();
         });

     });
     //The class lifetime can change. Now instantiated once.
     _c.Configure(a => a.ForSingletonOf<Interfaces.ILogger>());
     _c.Configure(a => a.For<Interfaces.IFacadeClass>().DecorateAllWith<facadeAOP>());

 In Ninject, the configuration is done this way:

_c =new StandardKernel();

_c.Bind(b => b.FromAssembliesMatching("Facade*.*")
          .SelectAllClasses()
          .BindDefaultInterfaces()
          .Configure(c => { c.InSingletonScope(); c.Intercept().With<facadeAOP>(); }));
_c.Bind(b => b.FromAssembliesMatching("Object*.*").SelectAllClasses().BindDefaultInterfaces());
_c.Bind(b => b.FromAssembliesMatching("Logger*.*").SelectAllClasses().BindDefaultInterfaces());

Both of these have used convention over configuration to map the class to the interface. They both have the facade class defined as a singleton and both decorate / Intercept the facade class with another class to wrap it in a try catch.

In Structuremap the facadeAOP class is defined as a class of the same type we are decorating:

public class facadeAOP : Interfaces.IFacadeClass
    {

        Interfaces.IFacadeClass ic;
        Interfaces.ILogger _log;

        //notice we inject the logger and facadeclass using IOC 
        public facadeAOP(Interfaces.IFacadeClass fc,Interfaces.ILogger log)
        {
            ic = fc;
            _log = log;
        }

        //We need to create an object which satisfies the interface
        public string StringHelloWorld()
        {
            try
            {
                //notice we execute the wrapped classes method in a try catch
                return ic.StringHelloWorld();
            }
            catch (Exception ex)
            {
                _log.Log(ex.Message, "AOP", Interfaces.LogLevel.Error);
                return "Error occured - " + ex.Message;
            }
            

        }
    }

For Ninject we accomplish in a similar fashion, however we change the class slightly to implements Ninjects interface of IInterceptor. Since we are using Ninjects Interface, we need to change appropriately.

 public class facadeAOP : IInterceptor
    {

        Interfaces.ILogger _log;

        //notice we still use IOC to pass in instance of logger 
        //but no longer pass in instance of facadeclass
        public facadeAOP(Interfaces.ILogger log)
        {
            _log = log;
        }

                
        //Satisfies the IInterceptor interface
        public void Intercept(IInvocation invocation)
        {
            try
            {
                //invoke the intercepted class
                invocation.Proceed();
            }
            catch (Exception ex)
            {
                _log.Log(ex.Message, "AOP", Interfaces.LogLevel.Error);
                Console.WriteLine("Error occured - " + ex.Message);
            }
        }
    }

The Ninject implementation can be used on all types of objects rather than structuremap which needed to create one for each interface being decorated.

I also needed to change to copy dll files to the bin directory:

copy $(SolutionDir)Injector\$(OutDir)ninject*.* $(SolutionDir)IOC\bin\Debug
copy $(SolutionDir)Injector\$(OutDir)linfu*.* $(SolutionDir)IOC\bin\Debug

E_IOC - Autofac

In Autofac, the configuration is done this way:

public ContainerBootstrapper()
{
   var builder = new ContainerBuilder();    
               
   //load the dlls and pass into the RegisterAssemblTypes
   var facade = Assembly.LoadFrom("FacadeClass.dll");
   var logger = Assembly.LoadFrom("LoggerClass.dll");
   var hello = Assembly.LoadFrom("ObjectHello.dll");
   var world = Assembly.LoadFrom("ObjectWorld.dll");

   //intercept the facade with a class that implements the class IInterceptor
   builder.RegisterAssemblyTypes(facade).
          As<Interfaces.IFacadeClass>().
          EnableInterfaceInterceptors().
          InterceptedBy(typeof(facadeAOP));

    //Create the Logger as a singleton 
    builder.RegisterAssemblyTypes(logger).As<Interfaces.ILogger>().SingleInstance();
    builder.RegisterAssemblyTypes(hello).As<Interfaces.IHelloClass>();
    builder.RegisterAssemblyTypes(world).As<Interfaces.IWorldClass>();

    //register a new facadeAOP class passing in a resolved logger to log with.
    builder.Register(r => new facadeAOP(r.Resolve<Interfaces.ILogger>()));
             
    //Build creates the container.
    _c = builder.Build();

 }

Autofac also uses an interecepter in a similar fashion as Ninject.

Note that you use a builder to register the components then Build to return a container.   

 

Unity - No sample

So one container I didn't touch was Unity from Microsoft.
The main reason was because of the switch by Structuremap to load using convention over configuration.

The Unity container uses XML configuration to map instances to concrete types.

The xml configuration is very similar to the 2.x version of structuremap and is similar to below.

Unity uses:

<configSections>
    <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, 
     Microsoft.Practices.Unity.Configuration"/>
</configSections> 

<unity xmlns="http://schemas.microsoft.com/practices/2010/unity">
       
    <container>
         <register type="IHelloClass" mapTo="ObjectHello.HelloClass"/>
         <register type="IWorldClass" mapTo="ObjectWorld.WorldClass" /> <!-- type mapping -->
         <register type="IFacadeClass mapTo="FacadeClass" />
         <register type="ILogger" mapTo="LoggerClass"> 
               <lifetime type="singleton" /> 
         </register>
    </container>

</unity>

Points of Interest

In the 2.X version of Structuremap, there was the ability to use XML configuration to read from the config file to configure itself. This has been removed as of version 3.0 and is not supported going forward.

I found building these projects in this way a nice way to learn, layering each one in such a way that your attention is focused on just the changes. Please drop a note if you find these helpful or if you have any questions.

The change to Ninject was simply understanding the differences between Ninject and Structuremap but no other changes in any other part of the program was made. 

History

Feb 1 - Initial release.

Feb 12 - Update with ninject

Feb 16 - Changed Ninject to correctly create the logger as a singleton. Added AutoFac solution

Feb 26 - Touch on Unity for IOC

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

DotNetSteve
Software Developer (Senior) Polaris Solutions
United States United States
Working in varied settings from small entrepreneurial companies to Fortune 500 companies. Skilled in analyzing client needs and developing solutions that are sound and effective.

Strong analytic capabilities with proven accomplishments in developing programs that exceed or meet stated goals, consistently work well, are easily maintained and fully documented. Versed in a number of SDLC technologies including Agile and Scrum, dedicated to deliver high quality software on time and on budget.

Experienced in helping companies and teams change their culture. Providing clear vision, asking tough questions of both developers and business, leading by example and building trust among all concerned.

You may also be interested in...

Pro
Pro

Comments and Discussions

 
GeneralMy vote of 5 Pin
Anil Kumar @AnilAwadh28-Feb-16 23:22
professionalAnil Kumar @AnilAwadh28-Feb-16 23:22 
GeneralRe: My vote of 5 Pin
DotNetSteve29-Feb-16 5:09
memberDotNetSteve29-Feb-16 5:09 
QuestionKeep the Great work going Pin
EngrBS1-Feb-16 18:52
professionalEngrBS1-Feb-16 18:52 
AnswerRe: Keep the Great work going Pin
DotNetSteve2-Feb-16 4:55
memberDotNetSteve2-Feb-16 4:55 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    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 | Terms of Use | Mobile
Web02 | 2.8.170118.1 | Last Updated 26 Feb 2016
Article Copyright 2016 by DotNetSteve
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid