Click here to Skip to main content
Click here to Skip to main content
Go to top

A Look at Fluent APIs

, 14 Jan 2011
Rate this:
Please Sign up or sign in to vote.
A look at Fluent APIs and an example of one.

Introduction

Of late, there has been a rise in the number of people using Fluent APIs, you literally see them everywhere, but what are these Fluent APIs? Where can I get me one of those?

Here is what our friend Wikipedia has to say on the matter:

In software engineering, a Fluent interface (as first coined by Eric Evans and Martin Fowler) is a way of implementing an object oriented API in a way that aims to provide for more readable code. A Fluent interface is normally implemented by using method chaining to relay the instruction context of a subsequent call (but a Fluent interface entails more than just method chaining). Generally, the context is defined through the return value of a called method self referential, where the new context is equivalent to the last context terminated through the return of a void context.

This style is marginally beneficial in readability due to its ability to provide a more fluid feel to the code, however, can be highly detrimental to debugging, as a Fluent chain constitutes a single statement, in which debuggers may not allow setting up intermediate breakpoints for instance.

-- http://en.wikipedia.org/wiki/Fluent_interface up on date 14/01/2011

This article will discuss the different types of Fluent APIs out there, and will also show you a demo app that includes a Fluent API of my own making, and shall also discuss some of the problems that you may encounter whilst creating your own Fluent API.

I should mention that this article is a very simple one (which makes a change for me), and I do not expect many people to like it, but I thought it would help some folk, so I published it any way. So if you read it and think jeez Sacha that was crap, just think back to this paragraph where I told you it would be a dead simple article.

Trust me, the next ones (they are in progress) are not so easy, and are quite hard to digest, so maybe this small one is a good thing.

To Fluent Or Not To Fluent

One of the main reasons to use Fluent APIs is (if they are well designed) that they follow the same natural language rules as we use, and as a result are a lot easier to use. I personally find them a lot easier to read, and the overall structure seems to leap out at me a lot better when I read a Fluent API.

That said, should all APIs be Fluent? Hell no, some APIs would be a right mess (too big, too many inter-dependant ordering), and let us not forget that Fluent APIs do take a little bit more time to develop, and might not be that easy to come up with, and your existing classes/methods may just not be that well suited to creating a Fluent API unless you started out with the intention of creating one in the first place. These are considerations you must take into account.

Looking at Some Example Fluent APIs

There are literally loads of Fluent interfaces out there (as I said, they are all the rage these days). I have chosen two specific ones that are outlined below. I have picked these two to talk about the different types of Fluent APIs that you may encounter.

Discussion Point 1: Fluent NHibernate

NHinernate is a well established ORM (Object Relational Mapper) for .NET. You would conventionally have a code file, let's say C#, and you would have typically configured an NHibernate mapping for this class you wish to persist using an NHibernate mapping XML file such as:

<?xml version="1.0" encoding="utf-8" ?>  
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"  
  namespace="QuickStart" assembly="QuickStart">  
 
  <class name="Cat" table="Cat">  
    <id name="Id">  
      <generator class="identity" />  
    </id>  
 
    <property name="Name">  
      <column name="Name" length="16" not-null="true" />  
    </property>  
    <property name="Sex" />  
    <many-to-one name="Mate" />  
    <bag name="Kittens">  
      <key column="mother_id" />  
        <one-to-many class="Cat" />  
      </bag>  
  </class>  
</hibernate-mapping>

You would have to produce one of these types of XML files per .NET class you wish to persist. Some folks out there thought, hey why not come up with a nice Fluent API that does the same thing, and Fluent NHibernate was born.

And here is an example of how we might Fluent NHibernate to configure a mapping for the type of Cat:

public class CatMap : ClassMap<Cat>
{
  public CatMap()
  {
    Id(x => x.Id);
    Map(x => x.Name)
      .Length(16)
      .Not.Nullable();
    Map(x => x.Sex);
    References(x => x.Mate);
    HasMany(x => x.Kittens);
  }
}

The thing that may not be obvious here is that we are not returning any values, or starting anything here, nor does the order seem to be that important; it would appear we are free to swap the order of the Fluent API around (though it is not recommended).

The Fluent NHibernate is just configuring something, so the order may not necessarily matter. There are, however, Fluent APIs where the order of the Fluent API terms applied is important, as we might be starting something that relies on previous Fluent API terms or even returns a value.

We will examine a Fluent API that starts something next, so the order of the Fluent API terms is of paramount importance.

Discussion Point 2: NServiceBus Bus Configuration

Another example is one for NServiceBus which configures its Bus like this:

Bus = NServiceBus.Configure.With()
    .DefaultBuilder()
    .XmlSerializer()
    .RijndaelEncryptionService()
    .MsmqTransport()
        .IsTransactional(false)
        .PurgeOnStartup(true)
    .UnicastBus()
        .ImpersonateSender(false)
    .LoadMessageHandlers() // need this to load MessageHandlers
    .CreateBus()
    .Start();

Note: The NServiceBus Fluent API assumes you will always end with a Start() method being called, as it is actually starting some internal object that relies on the values of the previous Fluent API terms being set.

The demo app I have included returns a value, so can be thought of as a similar example to the NServiceBus Fluent API; the ordering is important, but I will also show you how I deal with it if the user does not supply the Fluent API terms in the correct order (even though my fix is very specific to the demo app, you should still be able to see how to apply this logic to your own Fluent APIs).

The Demo Project, And Its Fluent API

For the attached demo app, I had to think of something to write a Fluent API for. I ended up picking something dead simple which is something that any WPF developer will have done on more than one occasion. So what did I choose to look at?

Quite simple really, I have written a deliberately simple Fluent API around obtaining a dummy set of data that can be fetched in a background TPL Task, and enables grouping and sorting to be specified and returns a ICollectionView which is used in a dead simple MainWindowViewModel that the MainWindow of the demo app uses.

Like I say, I deliberately set out to make a very simple Fluent API, so people could see the concept, it could be more elegant, but I wanted to oversimplify it so people could see how to craft their own Fluent APIs.

Here is a screenshot of the attached demo code running:

So let's have a look at the MainWindowViewModel code which is shown below. The most relevant bits of this code are the three ICommand.Execute() methods and the private helper methods these use.

using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Diagnostics;
using FluentDemo.Data.Common;
using FluentDemo.Providers;
using FluentDemo.Model;
using FluentDemo.Commands;
using System.Windows.Threading;
using System.Windows.Data;


namespace FluentDemo.ViewModels
{
    public class MainWindowViewModel : INPCBase
    {
        private ICollectionView demoData;

        public MainWindowViewModel()
        {
            ViewLoadedCommand = 
                new SimpleCommand<object, object>(ExecuteViewLoadedCommand);
            
            PopulateAsyncCommand = 
                new SimpleCommand<object, object>(ExecutePopulateAsyncCommand);
            
            PopulateSyncCommand = 
                new SimpleCommand<object, object>(ExecutePopulateSyncCommand);

            InCorrectFluentAPIOrderCommand = 
                new SimpleCommand<object, object>(
                ExecuteInCorrectFluentAPIOrderCommand);
            
        }

        private void SetDemoData(ICollectionView icv)
        {
            DemoData = icv;
        }

        public SimpleCommand<object, object> ViewLoadedCommand { get; private set; }
        public SimpleCommand<object, object> PopulateAsyncCommand { get; private set; }
        public SimpleCommand<object, object> PopulateSyncCommand { get; private set; }
        public SimpleCommand<object, object> 
               InCorrectFluentAPIOrderCommand { get; private set; }

        public ICollectionView DemoData
        {
            get
            {
                return demoData;
            }
            set
            {
                if (demoData != value)
                {
                    demoData = value;
                    NotifyPropertyChanged(new PropertyChangedEventArgs("DemoData"));
                }
            }
        }

        private void ExecuteViewLoadedCommand(object args)
        {
            PopulateSync();
        }

        private void ExecutePopulateAsyncCommand(object args)
        {
            PopulateAsync();
        }

        private void ExecutePopulateSyncCommand(object args)
        {
            PopulateSync();
        }

        private void ExecuteInCorrectFluentAPIOrderCommand(object args)
        {
            //NON-Threaded Version, with incorrect Fluent API ordering
            //Oh no, so how do we apply our sorting/grouping, we have missed
            //our opportunity, as when we call Run() we get a ICollectionView
            
            DemoData = new DummyModelDataProvider()
            //this actually returns ICollectionView,
            //so we are effectively at end of Fluent API calls
            .Run() 
            //But help is at hand, with some clever
            //extension methods on ICollectionView, we can preempt
            //the user doing this, and still get things
            //to work, and make it look like a fluent API
            .SortBy(x => x.LName, ListSortDirection.Ascending)
            .GroupBy(x => x.Location);
        }


        private void PopulateAsync()
        {
            //Threaded Version, with correct Fluent API ordering
            new DummyModelDataProvider()
                .IsThreaded()
                .SortBy(x => x.LName, ListSortDirection.Descending)
                .GroupBy(x => x.Location)
                .RunThreadedWithCallback(SetDemoData);
        }

        private void PopulateSync()
        {
            //NON-Threaded Version, with correct Fluent API ordering
            DemoData = new DummyModelDataProvider()
            .SortBy(x => x.LName, ListSortDirection.Descending)
            .GroupBy(x => x.Location)
            .Run();
        }
    }
}

See the simple Fluent API in action in the private methods above. Let's take the most complicated of these three examples and talk about it a bit before we go on to take a look at this article's simple Fluent API. The PopulateAsync() method is the most complicated, which is as shown below:

//Threaded Version, with correct Fluent API ordering
new DummyModelDataProvider()
    .IsThreaded()
    .SortBy(x => x.LName, ListSortDirection.Descending)
    .GroupBy(x => x.Location)
    .RunThreadedWithCallback(SetDemoData);

So what is going on there? Well, a few things:

  1. We are stating we want the DummyModelDataProvider to be run threaded, so we could reasonably assume that it will be run in the background.
  2. We are specifying that we want the results sorted using the LName field of the DummyModelDataProviders fetched data.
  3. We are specifying that we want the results grouped using the Location field of the DummyModelDataProviders fetched data.
  4. We are also supplying a callback for the threaded operation to call back to when it completes.

Now that reads pretty well, I think.

One thing to note though is that like the NServiceBus example we saw earlier, the order is important. If the RunThreadedWithCallback(..) method is called first, it would not be possible to use the other Fluent API terms. Or if we called the RunThreadedWithCallback(..) method before we called the IsThreaded(..) method, it would fail as the code does not yet know it has to run in the background. I know the IsThreaded(..) method is effectively redundant; we could infer that the code should be threaded if we are in the RunThreadedWithCallback(..) method, but as I said, this example is dumb to illustrate the dangers/advantages of Fluent APIs, so I made it totally stupid, with this redundant IsThreaded(..) method in there for that reason.

So how about we look at this demo app's Fluent API then (as Mr. T from the A-Team would say "quit your jibber jabber fool").

Well, here is it all, this is it in its entirety:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using FluentDemo.Data.Common;
using System.Linq.Expressions;
using FluentDemo.Model;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Data;

namespace FluentDemo.Providers
{
    public class SearchResult<T>
    {
        readonly T package;
        readonly Exception error;

        public T Package { get { return package; } }
        public Exception Error { get { return error; } }

        public SearchResult(T package, Exception error)
        {
            this.package = package;
            this.error = error;
        }
    }

    public class DummyModelDataProvider
    {
        #region Data
        private enum SortOrGroup { Sort=1, Group};
        private bool isThreaded = false;
        private string sortDescription = string.Empty;
        private string groupDescription = string.Empty;
        private ListSortDirection sortDirection = 
                ListSortDirection.Ascending;
        #endregion

        #region Fluent interface
        public DummyModelDataProvider IsThreaded()
        {
            isThreaded = true;
            return this;
        }

        /// <summary>
        /// SortBy
        /// </summary>
        public DummyModelDataProvider SortBy(
            Expression<Func<DummyModel, Object>> sortExpression, 
            ListSortDirection sortDirection)
        {
            this.sortDescription = 
                ObjectHelper.GetPropertyName(sortExpression);
            this.sortDirection = sortDirection;
            return this;
        }

        /// <summary>
        /// GroupBy
        /// </summary>
        public DummyModelDataProvider GroupBy(
            Expression<Func<DummyModel, Object>> groupExpression)
        {
            this.groupDescription = 
                ObjectHelper.GetPropertyName(groupExpression);
            return this;
        }


        public ICollectionView Run()
        {
            ICollectionView collectionView = 
                CollectionViewSource.GetDefaultView(GetItems(false));

            collectionView = ApplySortOrGroup(
                collectionView, SortOrGroup.Sort, sortDescription);
            collectionView = ApplySortOrGroup(
                collectionView, SortOrGroup.Group, groupDescription);

            return collectionView;
        }


        public void RunThreadedWithCallback(
            Action<ICollectionView> threadCallBack)
        {
            ICollectionView collectionView = null;

            if (threadCallBack == null)
                throw new ApplicationException("threadCallBack can not be null");
          
            GetAll(
                (data) =>
                {
                    if (data != null)
                    {
                        collectionView = CollectionViewSource.GetDefaultView(data);

                        collectionView = ApplySortOrGroup(
                            collectionView, SortOrGroup.Sort, sortDescription);
                        collectionView = ApplySortOrGroup(
                            collectionView, SortOrGroup.Group, groupDescription);
                    }
                    threadCallBack(collectionView);
                },
                (ex) =>
                {
                    throw ex;
                });
        }
        #endregion

        #region Private Methods
        private ICollectionView ApplySortOrGroup(ICollectionView icv, 
            SortOrGroup operation, string val)
        {
            if (string.IsNullOrEmpty(val))
                return icv;

            using (icv.DeferRefresh())
            {
                icv.GroupDescriptions.Clear();
                icv.SortDescriptions.Clear();

                switch (operation)
                {
                    case SortOrGroup.Sort:
                        icv.SortDescriptions.Add(
                            new SortDescription(val, sortDirection));
                        break;

                    case SortOrGroup.Group:
                        icv.GroupDescriptions.Add(
                            new PropertyGroupDescription(val, null, 
                            StringComparison.InvariantCultureIgnoreCase));
                        break;
                }
            }


            return icv;
        }


        //This is obviously just a simulated list, this would come from Web Service
        //or whatever source your data comes from
        private List<DummyModel> GetItems(bool isAsync) 
        {
            List<DummyModel> items = new List<DummyModel>();
            items.Add(new DummyModel("UK","sacha","barber", 
                isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
            items.Add(new DummyModel("UK", "sacha", "distell", 
                isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
            items.Add(new DummyModel("Greece","sam","bard", 
                isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
            items.Add(new DummyModel("Brazil","sarah","burns", 
                isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
            items.Add(new DummyModel("Australia", "gabriel","barnett", 
                isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
            items.Add(new DummyModel("Australia", "gabe", "burns", 
                isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
            items.Add(new DummyModel("Ireland", "hale","yeds", 
                isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
            items.Add(new DummyModel("New Zealand", "harlen","frets", 
                isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
            items.Add(new DummyModel("Australia", "ryan", "oberon", 
                isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
            items.Add(new DummyModel("Australia", "tim", "meadows", 
                isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
            items.Add(new DummyModel("Thailand", "dwayne", "zarconi", 
                isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));

            //add a few more if being called in async mode
            //just so user sees a change in the UI
            if (isAsync)
            {
                items.Add(new DummyModel("Australia", "elvis", "maandrake", 
                    isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
                items.Add(new DummyModel("Australia", "tony", "montana", 
                    isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
                items.Add(new DummyModel("Ireland", "esmerelda", "klakenhoffen", 
                    isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));

            }

            return items.ToList();
        }

        private void GetAll(
            Action<IEnumerable<DummyModel>> resultCallback, 
            Action<Exception> errorCallback)
        {
            Task<SearchResult<IEnumerable<DummyModel>>> task =
                Task.Factory.StartNew(() =>
                {
                    try
                    {
                        return new SearchResult<IEnumerable<DummyModel>>(
                                   GetItems(true), null);
                    }
                    catch (Exception ex)
                    {
                        return new SearchResult<IEnumerable<DummyModel>>(null, ex);
                    }
                });

            task.ContinueWith(r =>
            {
                if (r.Result.Error != null)
                {
                    errorCallback(r.Result.Error);
                }
                else
                {
                    resultCallback(r.Result.Package);
                }
            }, CancellationToken.None, TaskContinuationOptions.None,
                TaskScheduler.FromCurrentSynchronizationContext());
        }
        #endregion
    }
}

Which may look bad, but if we just show the Fluent API, look what we get:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using FluentDemo.Data.Common;
using System.Linq.Expressions;
using FluentDemo.Model;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Data;

namespace FluentDemo.Providers
{
    public class DummyModelDataProvider
    {
        #region Data
        private enum SortOrGroup { Sort=1, Group};
        private bool isThreaded = false;
        private string sortDescription = string.Empty;
        private string groupDescription = string.Empty;
        private ListSortDirection sortDirection = ListSortDirection.Ascending;
        #endregion

        #region Fluent interface
        public DummyModelDataProvider IsThreaded()
        {
            isThreaded = true;
            return this;
        }

        /// <summary>
        /// SortBy
        /// </summary>
        public DummyModelDataProvider SortBy(
            Expression<Func<DummyModel, Object>> sortExpression, 
            ListSortDirection sortDirection)
        {
            this.sortDescription = 
                ObjectHelper.GetPropertyName(sortExpression);
            this.sortDirection = sortDirection;
            return this;
        }


        /// <summary>
        /// GroupBy
        /// </summary>
        public DummyModelDataProvider GroupBy(
            Expression<Func<DummyModel, Object>> groupExpression)
        {
            this.groupDescription = 
                ObjectHelper.GetPropertyName(groupExpression);
            return this;
        }


        public ICollectionView Run()
        {
            ICollectionView collectionView = 
                CollectionViewSource.GetDefaultView(GetItems(false));

            collectionView = ApplySortOrGroup(
                collectionView, SortOrGroup.Sort, sortDescription);
            collectionView = ApplySortOrGroup(
                collectionView, SortOrGroup.Group, groupDescription);

            return collectionView;
        }

        public void RunThreadedWithCallback(
            Action<ICollectionView> threadCallBack)
        {
            ICollectionView collectionView = null;

            if (threadCallBack == null)
                throw new ApplicationException("threadCallBack can not be null");
          
            GetAll(
                (data) =>
                {
                    if (data != null)
                    {
                        collectionView = CollectionViewSource.GetDefaultView(data);

                        collectionView = ApplySortOrGroup(
                            collectionView, SortOrGroup.Sort, sortDescription);
                        collectionView = ApplySortOrGroup(
                            collectionView, SortOrGroup.Group, groupDescription);
                    }
                    threadCallBack(collectionView);
                },
                (ex) =>
                {
                    throw ex;
                });
        }
        #endregion

    }
}

See how easy that has become in the following methods? All we actually do is set an internal field to represent the action of calling the Fluent API method, and return ourselves (this):

  • IsThreaded()
  • SortBy()
  • GroupBy()

The last part of the Fluent API are the Run() or RunThreadedWithCallback() methods; these are expected to be the final methods called. Now, there is nothing to stop the user calling things in any order they want, so they could completely bypass the:

  • IsThreaded()
  • SortBy()
  • GroupBy()

method calls entirely, and just call the Run() or RunThreadedWithCallback() which returns an ICollectionView and there is nothing we can do to stop that. We can, however, use another .NET trick, which is Extension Methods, so we can make it look like a Fluent API, even after they have bypassed the normal Fluent API ordering.

There is an example of this in the demo app, where I deliberately do not follow the demo app's Fluent API and call the Run() method too early, which returns an UnSorted/UnGrouped ICollectionView. Here is that code:

DemoData = new DummyModelDataProvider()
//this actually returns ICollectionView,
//so we are effectively at end of Fluent API calls
.Run() 
//But help is at hand, with some clever
//extension methods on ICollectionView, we can preempt
//the user doing this, and still get things to work,
//and make it look like a fluent API
.SortBy(x => x.LName, ListSortDirection.Ascending)
.GroupBy(x => x.Location);

But the demo app also provides some extension methods to ICollectionView which kind of preempt someone doing this, and as we can see from the snippet above, the Fluent API'ness is still preserved.

Here are the relevant ICollectionView Extension Methods. It's a cheap parlour trick, but it works in this case, and is something you could use in your own Fluent APIs:

public static class ProviderExtensions
{
    public static ICollectionView SortBy(this ICollectionView icv, 
           Expression<Func<DummyModel, Object>> sortExpression, 
           ListSortDirection sortDirection)
    {
        icv.SortDescriptions.Add(new SortDescription(
            ObjectHelper.GetPropertyName(sortExpression), sortDirection));
        return icv;
    }

    public static ICollectionView GroupBy(this ICollectionView icv, 
           Expression<Func<DummyModel, Object>> groupExpression)
    {
        icv.GroupDescriptions.Add(
            new PropertyGroupDescription(
                ObjectHelper.GetPropertyName(groupExpression)));
        return icv;
    }
}

That's It

Anyway, that is it for now. I know it is not my normal style article, but I do like to write stuff like this too, so if you feel like voting/commenting, please go ahead, gratefully received.

License

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

Share

About the Author

Sacha Barber
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)
 
- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence
 
Both of these at Sussex University UK.
 
Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

You may also be interested in...

Comments and Discussions

 
GeneralMy vote of 5 PinmemberAshutosh Phoujdar3-Jul-13 20:36 
GeneralMy vote of 5 Pinmemberfredatcodeproject14-May-13 1:21 
GeneralRe: My vote of 5 PinmvpSacha Barber14-May-13 2:15 
GeneralFluent API + Formatting [modified] PinmemberOleg Shilo15-Aug-12 19:48 
GeneralRe: Fluent API + Formatting PinmvpSacha Barber15-Aug-12 21:44 
GeneralRe: Fluent API + Formatting PinmemberOleg Shilo19-Aug-12 3:23 
GeneralThank you! PinmemberMMuazzamAli3-Nov-11 11:19 
GeneralRe: Thank you! PinmvpSacha Barber3-Nov-11 20:22 
QuestionGreat article! Tank you, Sacha. PinmemberJürgen Bäurle19-Sep-11 7:14 
GeneralGreat article on an interesting topic. PinmemberAlexander Wieser19-Feb-11 2:45 
GeneralRe: Great article on an interesting topic. PinmvpSacha Barber19-Sep-11 9:26 
GeneralMy vote of 5 PinmemberMichael Agroskin13-Feb-11 12:49 
GeneralMy vote of 5 PinmemberDr.Luiji11-Feb-11 12:08 
GeneralRe: My vote of 5 PinmvpSacha Barber12-Feb-11 0:22 
GeneralLike it PinmemberCIDev11-Feb-11 9:15 
GeneralRe: Like it PinmvpSacha Barber12-Feb-11 0:21 
GeneralMy vote of 5 Pinmember69Icaro9-Feb-11 2:46 
GeneralRe: My vote of 5 PinmvpSacha Barber12-Feb-11 0:21 
GeneralMy Vote of 5 PinmemberRaviRanjankr8-Feb-11 19:11 
GeneralRe: My Vote of 5 PinmvpSacha Barber8-Feb-11 19:56 
GeneralMy vote of 5 PinmemberJF20158-Feb-11 8:54 
GeneralRe: My vote of 5 PinmvpSacha Barber8-Feb-11 19:56 
GeneralMy vote of 5 PinmvpAbhijit Jana19-Jan-11 1:57 
GeneralRe: My vote of 5 PinmvpSacha Barber19-Jan-11 9:13 
GeneralMy vote of 5 Pinmemberutard gilles18-Jan-11 23:38 

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
Web03 | 2.8.140916.1 | Last Updated 14 Jan 2011
Article Copyright 2011 by Sacha Barber
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid