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

Diagnostic Explorer

, 29 Nov 2010
Rate this:
Please Sign up or sign in to vote.
A .NET library and website which allows developers to expose and view arbitrary diagnostic information about their .NET processes.

Table of Contents

** Brand spanking new feature

Introduction

Diagnostic Explorer is a .NET library and website which allows developers to expose and view arbitrary diagnostic information about their .NET processes. Properties of .NET objects constructed and held in memory can be easily exposed and viewed alongside logging information generated by frameworks such as log4net. I have been developing and using Diagnostic Explorer in production systems for the last 5 years, and found it an invaluable tool for monitoring .NET processes and quickly solving issues.

Background

The project started as an attempt to find out what was happening inside a Windows Service I was working on in 2005. The service ran fine when all was well, but if something went wrong, then extensive detective work was required to diagnose the problem. The only question that could be asked of the Windows Service was "Is it running?".

Various logging frameworks exist, such as log4net and Microsoft's Logging Application Block, and performance counters can be used to expose performance metrics for certain types of data. I found existing techniques to be unwieldy, so I wrote a simple website which used a Remoting interface to fetch and display a collection of key/value properties from the Windows Service. The project evolved as I realised how to make this technique more generic, and several technologies later (Remoting, HTML, AJAX, WinForms, WPF, and finally WCF/Silverlight), Diagnostic Explorer is the result.

How it Works

Any .NET process can expose diagnostics by referencing the DiagnosticExplorer.dll assembly. This assembly contains all that is required to make your application appear in the Silverlight Viewer pictured above and expose properties from any managed .NET object you wish.

First, your application uses the DiagnosticHostingService class to host a WCF Service from which diagnostic properties, values, and logging messages can be requested. The service can be configured to use a specific port, or a random free port number (see Exposing Diagnostics below).

Second, your application uses the DiagnosticManager class to register .NET objects whose properties will be exposed, and the EventSink class or a logging framework (see Using log4net below) to expose logging messages.

When you select your application in the Silverlight Viewer, the WCF Service hosted by your application is polled every second in order to retrieve and display diagnostic information.

Registration

The Diagnostic Web Service maintains a list (actually, a folder structure) of all registered applications. When your application starts up, how does the Diagnostic Web Service know it is running and what its endpoint address is? There are two options:

  • A right-click context menu in the Silverlight Viewer enables you to manually register an application by providing its name and endpoint address. For this to work though, the application must be using a fixed port number.
  • DiagnosticHostingService can be configured to register its own name and endpoint address with the Diagnostic Web Service. This is mandatory if using a random port number.

Exposing Diagnostics

Two lines of code (plus a little configuration) are all it takes to start exposing diagnostics in your own application. You must first reference DiagnosticExplorer.dll and use the DiagnosticHostingService.Start() and DiagnosticHostingService.Stop() methods. This class is responsible for hosting the WCF Service and registering with the Diagnostic Web Service:

using DiagnosticExplorer;
...
public Form1()
{
    InitializeComponent();
    DiagnosticHostingService.Start();
    FormClosed += StopDiagnostics;
}

private void StopDiagnostics(object sender, EventArgs e)
{
    DiagnosticHostingService.Stop();
}

If DiagnosticHostingService is to register itself, it must know how to contact the Diagnostic Web Service:

<configuration>
    <system.serviceModel>
    <client>

        <endpoint address= 
           "http://localhost/Diagnostics/RegistrationService.svc"
         binding="wsHttpBinding" 
         bindingConfiguration= "WSHttpBinding_IRegistrationService"
         contract="DiagnosticExplorer.IRegistrationService"
         name="WSHttpBinding_IRegistrationService" />

    </client>
    <bindings>
        <wsHttpBinding>
            <binding name="WSHttpBinding_IRegistrationService">
                <security mode="Message" />

            </binding>
        </wsHttpBinding>
    </bindings>
    </system.serviceModel>
</configuration>

An optional configuration section can be used to customise the behaviour of the DiagnosticHostingService:

<configuration>
    <configSections>
        <section name="diagnosticExplorer"
            type="DiagnosticExplorer.DiagnosticSectionHandler,
                  DiagnosticExplorer" />
    </configSections>

    <diagnosticExplorer>
        <!-- Used to differentiate between multiple instances -->

        <!-- of the same application running on a machine (optional) -->
        <instanceName>Dev</instanceName>

        <!-- Auto or Manual port number selection -->

        <portMode>Auto</portMode>

        <!-- Port to use when portMode is Manual -->
        <port>12345</port>

        <!-- True if this application should register
            itself with the diagnostics web service -->

        <autoRegister>true</autoRegister>
    </diagnosticExplorer>
</configuration>

When you run your application and the DiagnosticHostingService has been started, some limited system information will already be visible in the Silverlight Viewer.

Sample2.gif

Exposing Diagnostics (Web Applications)

When exposing diagnostics from a web application hosted in IIS, DiagnosticHostingService can be used as normal in the Global.asax Application_Start and Application_End methods, or you can add a WCF Service to your application and manually register the URL.

  1. Copy the WebDiagnostics.svc file from the Diagnostic Explorer's Web Service (the DiagnosticWeb folder in the download). Place this somewhere in your own web application (there is no code-behind).
  2. Copy the service's WCF configuration from the <system.serviceModel> section in web.config. You will need:
    • <services><service name="WebDiagnostics" ...
    • <behaviors><serviceBehaviors><behavior name="WebDiagnostics" ...
    • <bindings><wsHttpBinding><binding name="WebDiagnostics" ...
  3. Check that you can now browse to the new service by typing its URL into your browser; e.g.: http://localhost/MyWebApp/WebDiagnostics.svc.
  4. In the Diagnostic Explorer Silverlight Viewer, right-click in the left hand panel and choose "Register Process". Type in a name and the URL for your WebDiagnostics service.

Exposing Properties

It is very easy to start exposing your own information using the DiagnosticManager.Register(obj, name, category) method. Just select an object which you think has some interesting properties and register it:

public partial class Form1 : Form
{
    private SomeClass myObject;

    public Form1()
    {
        InitializeComponent();
        DiagnosticHostingService.Start();
        FormClosed += (sender, e) => DiagnosticHostingService.Stop();

        myObject = new SomeClass();
        DiagnosticManager.Register(myObject, 
                    "My  Object", "My Category");

    }

    class SomeClass
    {
        public string MyProp1 { get { return "A Value"; } }
        public string MyProp2 { get { return "A Different Value"; } }
    }
}

When you run the application, the Silverlight Viewer should now look like this:

Sample3.gif

Note that it is necessary to maintain a reference to any object which you register with DiagnosticManager. This is because DiagnosticManager holds only a WeakReference to your object and so will not prevent garbage collection. This helps to avoid memory leaks which might otherwise occur, and removes the need to call DiagnosticManager.Unregister(myClass). Instead, your registered object will disappear from the Silverlight Viewer as soon as it is garbage collected.

Exposing Editable Properties

Add the [Property(AllowSet = true)] attribute above any property, and it will be editable from the Diagnostic Explorer. Alternatively, you can specify that all properties in a class are editable by applying the [DiagnosticClass(AllPropertiesSettable = true)] attribute above your class declaration. Editable properties are indicated by a blue foreground.

Diagnostic Explorer will attempt to set the property using Convert.ChangeType. If that fails and a static Parse(string) method exists on the property type, that will be invoked. Failing that, an error message is displayed to the user.

[Property(AllowSet = true)]
public string Name
{
    get { return _name; }
    set
    {
        _name = value;
        OnPropertyChanged("Name");
    }
}

Exposing Methods

Put [DiagnosticMethod] above any method belonging to any object which is visible in diagnostics, and a yellow star will be shown next to it. You can now click and invoke any registered method.

[DiagnosticMethod]
public string SayHelloSync(string caption, string message)
{
    if (message == "throw")
        throw new ArgumentException("Ok, I'll throw");

    Stopwatch watch = Stopwatch.StartNew();
    Action sayHello = () => MessageBox.Show(this, message, caption, 
                              MessageBoxButtons.OK, MessageBoxIcon.Asterisk);
    Invoke(sayHello);
    return string.Format("User clicked Ok in {0:N1} seconds", watch.Elapsed.TotalSeconds);
}

Exposing Events

There are two ways of exposing events through the Silverlight Viewer.

Using EventSink

The first way is to use the EventSink class directly from your code. If you add the following code to Form1 as above, the Silverlight Viewer should now look like this:

Sample4.gif

private EventSink mySink;
...
mySink = EventSink.GetSink("My Events", "My Category");
mySink.LogEvent(EventSeverity.Low, "Low severity issue", 
                "Low severity detail");
mySink.LogEvent(EventSeverity.Medium, "Medium severity issue", 
                "Medium severity detail");
mySink.LogEvent(EventSeverity.High, "High severity issue", 
                "High severity detail");

Using log4net

The preferred and much more flexible method, however, is to use a logging framework such as log4net:

using log4net;
...
ILog log = LogManager.GetLogger(typeof(Form1));
log.Info("This is an INFO level log message");
log.Warn("This is a WARN level log message", myException);
log.Error("This is an ERROR level log message\n", myException);

In the log4net configuration file, you must add an appender of type DiagnosticExplorer.Log4Net.DiagnosticAppender and configure it with the desired SinkName and SinkCategory. This appender will use the EventSink class when instructed by the log4net framework to log an event.

<appender name="DiagnosticExplorer_Form1"
          type="DiagnosticExplorer.Log4Net.DiagnosticAppender, DiagnosticExplorer">
    <SinkName>My Events</SinkName>
    <SinkCategory>My Category</SinkCategory>

    
    <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%message" />
    </layout>
</appender>

Security???

I realise that there are horrendous security implications in allowing people to edit properties and invoke methods on (potentially production) processes which expose diagnostics. I will address this gaping security hole in my next release.

Setting it Up

Before you run Diagnostic Explorer, make sure you have installed the Silverlight 4 Tools.

Create Diagnostic Web Service

  1. Create a web application called Diagnostics in IIS based on the DiagnosticWeb folder in the sample download. If you are working with the source code, this is the DiagnosticExplorer.Web project in the solution.
  2. Check that you can browse to (localhost presumed):
    • http://localhost/Diagnostics/RegistrationService.svc
    • http://localhost/Diagnostics/Diagnostics.svc
    • http://localhost/Diagnostics/WebDiagnostics.svc
  3. Check that you can run the Silverlight Viewer by browsing to:
    • http://localhost/Diagnostics/Default.aspx

Configure WidgetSample

  1. Edit the App.config file of the WidgetSample provided. This is WidgetSample\WidgetSample.exe.config in the sample download; otherwise the WidgetSample project. Make sure that the client endpoint address of the registration service is correct.
  2. Run WidgetSample.
  3. Take a look at the Silverlight Viewer and verify that the sample app appears. If it doesn't, run DebugView to see if any errors are reported, and troubleshoot from there.

Set Up "Retro Diagnostics" (Optional)

This part is a little more difficult than the last two steps. What we want to achieve is for the storage of logging events so that they can be retrieved and displayed in the Retro tab of the Silverlight Viewer.

It would be possible to write a log4net appender which inserts logging events directly into a database. I don't want my appenders writing to a database, however; so I have an appender which writes to a WCF Service over MSMQ. A Windows Service then reads the messages from the queue using a netMsmqBinding and inserts them into a database, from where the Diagnostic Web Service can read them.

This sounds like too many points of failure for a sample application, but here goes:

Create Database and Message Queue

  1. Create a database called Diagnostics. The script CreateDatabase.sql is included in the solution and the sample download.
  2. Create a transactional message queue called Diagnostics. Make sure it has permissions so that the log4net appender in the sample app can write, and the Windows Service in the next step can read.

Install the Windows Service

  1. Use installutil.exe to install the Diagnostic Explorer Windows Service.
  2. Make sure the config file for the Windows Service contains the correct database connection string, correct MSMQ path in the <system.serviceModel> section, and that the RegistrationService endpoint is correct.
  3. Start the Diagnostic Explorer Service from the service control panel. Check the Windows Service has registered itself in Diagnostics using the Silverlight Viewer.

Configure and Run WidgetSample

  1. Check the log4net.xml configuration file in the WidgetSample. Make sure <appender-ref ref="MsmqLogger" /> is not commented out. It is configured to use the LoggingService_MSMQ endpoint, so verify that the LoggingService_MSMQ endpoint has the correct queue path in App.config.
  2. Run WidgetSample and verify that it appears in Silverlight Viewer. Generate a few events by clicking some buttons.
  3. Verify that the Windows Service is successfully reading the events from the message queue. You can do this by querying the database, or by viewing the Windows Service in the Silverlight Viewer.

View "Retro" Diagnostics

  1. Ensure that the database connection string in Web.config is correct in the web application.
  2. Select the Retro tab in the Silverlight Viewer and click the Refresh button. If all is well, you should see events which have been logged to the database.

Note that if you want to provide your own way of persisting and retrieving events, you can implement ILogWriter and ILogReader found in DiagnosticExplorer.dll. Place your own implementation inside the Windows Service and web application bin directories, and alter the App.config and Web.config files accordingly:

<!--
<add key="ILogReader" value="DiagnosticExplorer.SqlLoggingService,
    DiagnosticExplorer.Common" />
<add key="ILogWriter" value="DiagnosticExplorer.SqlLoggingService,
    DiagnosticExplorer.Common" />

-->

<add key="ILogReader" value="MyLogReader, MyAssembly" />
<add key="ILogWriter" value="MyLogWriter, MyAssembly" />

Advanced Features

Expose Static Properties

If you have a class which contains some interesting static properties, you can register it just as you would an object instance.

Snippet1.gif

DiagnosticManager.Register(typeof(MyClass), 
                  "Static Stuff", "My Category");
...

public class MyClass
{
    public static string Hello { get { return "World"; } }
    public static string Goodbye { get { return "Cruel World"; } }
}

Stop a Property from Being Exposed in Diagnostics

To stop a specific property from being exposed in Diagnostics, use PropertyAttribute with Ignore = true. Alternatively, use the [Browsable(false)] attribute.

Snippet2.gif

class SomeClass
{
    public string MyProp1 { get { return "I'm not ignored"; } }

    //Won't be visible in diagnostics

    [Property(Ignore = true)]
    public string MyProp2 { get { return "I am ignored"; } }
}

Make Only Specific Properties Show in Diagnostics

By using DiagnosticClassAttribute, you can specify that only properties which are explicitly marked with PropertyAttribute or [Browsable(true)] should be exposed in Diagnostics.

Snippet3.gif

[DiagnosticClass(AttributedPropertiesOnly = true)]
class SomeClass
{
    [Property]
    public string MyProp1 { get { return "I am attributed :)"; } }

    //Won't be visible in diagnostics

    public string MyProp2 { get { return "I'm not attributed :("; } }
}

Prevent Inherited Properties Showing

If your diagnostic class is a subclass and you don't want any inherited properties to show, use the [DiagnosticClass(DeclaringTypeOnly = true)] attribute. Note that if the superclass were to use this attribute too, then its properties would be exposed by the subclass.

public class Class1
{
    [Browsable(true)]
    public string PropA
        { get { return "I'm not exposed despite being browsable"; } }
}

[DiagnosticClass(AttributedPropertiesOnly = true, DeclaringTypeOnly = true)]

public class Class2 : Class1
{
    [Browsable(true)]
    public string PropB { get { return "I am exposed"; } }

    [Property]
    public string PropC { get { return "I am exposed too"; } }

    public string PropD { get { return "I'm not exposed (no attribute)"; } }
}

Customise How Properties are Displayed

The PropertyAttribute can be used to customise how you want properties to be exposed.

Snippet4.gif

class SomeClass
{
    public string MyProp1 { get { return "A Value"; } }

    [Property(Name = "Alternate name",
        Category = "Some Category",
        Description = "This appears in the tooltip",
        FormatString = "Value is: {0:N3}")]
    public double MyIntValue { get { return 12345.6789; } }
}

Note that the ComponentModel attributes [Category("Some Category")] and [Description("This appears in the tooltip")] can also be used.

Expose a Complex Property in its Own Category

The default behaviour when turning an object's property into a string is to use Convert.ToString or string.Format where a format string is specified. For properties which are themselves complex objects, this behaviour can be modified with the ExtendedPropertyAttribute.

Snippet10.gif

class SomeClass
{
    public string MyProp1 { get { return "A Value"; } }

    [ExtendedProperty]
    public Person SomePerson{ get; set; }
}

Exposing Collections

Collection properties can be displayed in diagnostics. By default, the code below will look like this in Diagnostics:

Snippet5.gif

class SomeClass
{
    public string MyProp1 { get { return "A Value"; } }

    //Contains 12 items
    public IList<Person> People { get { return _myListOfPeople; } }
}

class Person
{
    public string Name { get; set; }
    public int Height { get; set; }
    public string Nationality { get; set; }

    public override string ToString()
    {
        return string.Format("{0}, {1}cm, {2}", Name, Height, Nationality);
    }
}

Applying CollectionPropertyAttribute to a collection property can be used to customise how collections are displayed. Click on the links below to see how the attribute affects behaviour.

[CollectionProperty(CollectionMode.Count)]

Snippet6a.gif

[CollectionProperty(CollectionMode.Count, FormatString="There are {0:N2} people")]

Snippet6b.gif

[CollectionProperty(CollectionMode.List)]

Snippet7a.gif

[CollectionProperty(CollectionMode.Concatenate, Separator = " --> ", MaxItems = 3)]

Snippet9a.gif

[CollectionProperty(CollectionMode.Concatenate, Separator = " --> ", MaxItems=2, FormatString="Person({0})")]

Snippet9b.gif

[CollectionProperty(CollectionMode.List, Category="My People", NameProperty="Name")]

Snippet7c.gif

[CollectionProperty(CollectionMode.Categories)]

Snippet8b.gif

[CollectionProperty(CollectionMode.Categories, CategoryProperty = "Name", Category="People")]

Snippet8c.gif

Exposing Rates

As an aid to capturing event rates in your code, the RateCounter class is provided. It's like a light-weight performance counter which exposes to Diagnostics an average of the number of signals received over a number of seconds.

The code below looks like this when viewed in Diagnostics:

Snippet11a.gif

If you were to uncomment the RateProperty attribute, it would look like this:

Snippet11b.gif

class SomeClass
{
    private System.Timers.Timer _myTimer = new System.Timers.Timer(100);
    private Random _rand = new Random();

    public SomeClass()
    {
        //Construct a rate counter which reports average over 5 seconds

        ThingsHappened = new RateCounter(5);

        _myTimer.Elapsed += (sender, e) => ThingsHappened.Register
                        (_rand.Next(1, 100));
        _myTimer.Start();
    }

    public string MyProp1 { get { return "A Value"; } }

    //[RateProperty(ExposeRate = true, ExposeTotal = true)]

    public RateCounter ThingsHappened { get; private set; }
}

TraceScope

When trying to troubleshoot a problem remotely, it would be nice to know how code is performing: which methods are being called, in what sequence, what are the important things that are happening, and how much time is it all taking. You can log events and examine the fallout, but this is especially hard when you have multiple operations going on at the same time. Maybe you have multiple threads all running the same code and you have to somehow determine which log messages belong to which thread.

To solve this problem, I have included in DiagnosticExplorer.dll a class called TraceScope. It is a class which sets up a thread-static "ambient" scope which collects trace information. It should always be constructed in a using block as shown below, and a Action<string> can be passed in to the constructor. When the TraceScope is disposed, it creates a string representation of the trace information it has collected and passes it to Action<string>.

You can call the static method TraceScope.Trace("My debug message") from anywhere in any assembly, and if there is an ambient TraceScope, the message will be appended to it. If you create a TraceScope and there is already an ambient TraceScope, the scopes will be nested.

Consider the code below and take a look at the TraceScopeExample class which is referenced. See here. In the TraceScope constructor, I am passing the ILog.Info(string) method of a log4net logger, meaning that when the scope is disposed, the _formLog.Info method will be called with the results of the TraceScope, which you can see here:

private static ILog _formLog = LogManager.GetLogger(typeof (Form1));

private void btnTraceScope_Click(object sender, EventArgs e)
{
    using (new TraceScope(_formLog.Info))
    {
        TraceScope.Trace("In Trace Scope Button Click");
        TraceScopeExample.TestTraceScope1();
    }

    MessageBox.Show("Just generated a trace scope. Check diagnostics.");
}

The output of a TraceScope is an indented text representation of the path your code took through various scopes and all the trace messages which were collected along the way. The numbers you see in square brackets are the total time taken since the outermost scope was created and the time which has elapsed since the last trace message.

The Silverlight Viewer recognizes when a log message contains the result of a TraceScope. It parses it and provides a user friendly display, and tries to highlight the execution paths which took up the most time. You can see how it looks in the viewer.

TraceScope.gif

I think it could be improved, so any ideas are welcome.

Customising Behaviour

The web.config file contains some customisable settings.

<appSettings>

    <-- Time after which Auto registered processes renew their registrations -->

    <add key="RenewTime" value="00:00:20" />
    <-- The frequency (ms) at which the Silverlight Viewer polls a process -->
    <add key="PollTime" value="1000" />

    <-- The frequency (ms) at which the Silverlight Viewer
                refreshes the process menu -->
    <add key="MenuRefresh" value="2000" />
    <!-- The implementations of ILogReader and ILogWriter
        which are used to read and write "retro" events -->

    <add key="ILogReader" value="DiagnosticExplorer.SqlLoggingService,
        DiagnosticExplorer.Common" />
    <add key="ILogWriter" value="DiagnosticExplorer.SqlLoggingService,
        DiagnosticExplorer.Common" />
</appSettings>

Two of these settings, PollTime and MenuRefresh, can be overridden in the URL http://localhost/Diagnostics/Default.aspx?PollTime=1500&MenuRefresh=5000.

Comments and To Do

While I mentioned that this system is used in a production environment, the version presented here is the first version to use WCF, automatic port selection, and self-registration with the Web Service. In the previous version, .NET Remoting was used for communication, port selection was manual, and you had to manually update an XML file to register applications. This has the disadvantage of not being able to start multiple instances of the same process, as the port will be taken by the first to start.

There is a small bug in the Retro tab where the Refresh button doesn't work if you have clicked on one of the items in the grid.

The source code is hosted at http://sourceforge.net/projects/diagexplorer/. If you are at all interested and have some ideas, I would welcome contributors.

History

  • 4th March, 2010
    • Initial version.
  • 14th March, 2010: Some changes were made to remove explicit use of MSMQ for logging messages:
    • Removed the MessageQueueAppender log4net appender. It has been replaced with LoggingServiceAppender which uses an ILogWriter service over WCF, so you can now use any transport you want between your application and the logging service.
    • The message queue which must be created for the Retro feature must now be a transactional queue.
    • The IEventIO interface has been replaced by ILogReader and ILogWriter.
  • 28th March, 2010
    • Converted to Visual Studio 2010 and Silverlight 4 RC.
    • Added support for ComponentModel attributes: BrowsableAttribute, CategoryAttribute, and DescriptionAttribute.
  • 24th October, 2010
    • Converted to Silverlight Toolkit Apr. 2010.
    • Improved context menus.
    • Improved Rename and New Registration popups.
    • Wrapped LoggingServiceAppender's Send method with new TransactionScope(TransactionScopeOption.Suppress) to stop interference with transactions.
    • Fixed a couple of minor UI bugs.
  • 7th November, 2010
    • Improved support and documentation for exposing Diagnostics in web applications.
    • Disabled drag/drop in the left hand tree as the Silverlight Toolkit tree randomly starts drag operations. (Re-enable it with the context menu.)
    • Recut all my images so they're not resized by CodeProject, hopefully.
  • 27th November, 2010
    • You can now edit properties and call methods from Diagnostic Explorer!
    • Tabs on right hand side now change colour when events arrive.

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)

Share

About the Author

cameron_elliot
Software Developer
United Kingdom United Kingdom
I am a software developer originally from Auckland, New Zealand. I have lived and worked in London since 2005.

Comments and Discussions

 
GeneralMy vote of 5 PinmemberPerry Bruins10-Dec-10 22:15 
GeneralRe: My vote of 5 Pinmembercameron_elliot12-Dec-10 3:14 
GeneralMy vote of 5 Pinmemberlinuxjr12-Nov-10 12:30 
GeneralMy vote of 5 Pinmemberjim lahey12-Nov-10 1:39 
GeneralMy vote of 5 PinmemberSlacker0079-Nov-10 4:59 
GeneralThis is excellent work!!!! Pinmemberxzz01952-Nov-10 3:01 
GeneralMost Interesting PinmemberKenJohnson30-Mar-10 4:13 
GeneralRe: Most Interesting Pinmembercameron_elliot30-Mar-10 5:44 
GeneralExactly what I was waiting for ! PinmemberWilliamSauron19-Mar-10 21:54 
GeneralRe: Exactly what I was waiting for ! Pinmembercameron_elliot20-Mar-10 9:06 
GeneralRe: Exactly what I was waiting for ! PinmemberWilliamSauron20-Mar-10 22:07 
GeneralRe: Exactly what I was waiting for ! Pinmembercameron_elliot20-Mar-10 22:56 
GeneralRe: Exactly what I was waiting for ! PinmemberWilliamSauron21-Mar-10 4:54 
GeneralRe: Exactly what I was waiting for ! PinmemberWilliamSauron21-Mar-10 6:57 
GeneralRe: Exactly what I was waiting for ! Pinmembercameron_elliot21-Mar-10 22:16 
GeneralRe: Exactly what I was waiting for ! PinmemberWilliamSauron22-Mar-10 21:18 
GeneralRe: Exactly what I was waiting for ! Pinmembercameron_elliot29-Mar-10 6:01 
GeneralRe: Exactly what I was waiting for ! PinmemberWilliamSauron30-Mar-10 6:33 
GeneralRe: Exactly what I was waiting for ! Pinmembercameron_elliot29-Nov-10 9:26 
GeneralRe: Exactly what I was waiting for ! PinmemberWilliamSauron30-Nov-10 4:59 
GeneralAwsome! Pinmemberkazza_roc15-Mar-10 0:24 
GeneralRe: Awsome! Pinmembercameron_elliot15-Mar-10 2:43 
GeneralRe: Awsome! Pinmemberkazza_roc15-Mar-10 3:14 
GeneralGreat work! PinmemberJanusPien8-Mar-10 11:12 
QuestionOverhead? Pinmemberstevenmcohn8-Mar-10 3:44 

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 29 Nov 2010
Article Copyright 2010 by cameron_elliot
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid