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

Overriding App.Config Settings for Multiple Objects

, 17 May 2008
Rate this:
Please Sign up or sign in to vote.
One method for loading common and object-specific data from your app.config file.
ConsoleTester

Introduction

During the last eight months, I've been writing a LOT of windows services at work. Recently, I had a need to write a multi-service assembly. All of the services retrieved the app.config file, but each one required a different service-related value for the same setting. In other words, the value for the key "Test1" for Service1 would be different than the value for the same key for Service2.

Now, I didn't need anything exhaustive, and after doing a little (okay - VERY little) research via Google, I decided it would just be faster to do it my way. It just seemed to me that adding SectionGroups and Sections required to much delicate manual modification of the app.config file, and also required moderate changes to my already existing code base. I guess I'm admitting that there's a more "correct" way to support multiple versions of settings in a single app.config file (using SectionGroups and Sections), but the maintenance headaches would exceed the usefulness in no time.

What I Started With

When I first started writing .Net apps (just eight months ago), I decided it might be a good idea to write a class that handled all of the app.config file stuff. The reasoning is that (in our applications) the app.config is loaded when the service starts (or restarts), and is generally not changed (or reloaded) while the service is running. I acknowledge the fact that some of you might in fact modify your app.config files on the fly. While that's solidly outside the scope of this article, it shouldn't be a difficult matter to modify this code to support your requirements in that regard. Personally, I feel that modifying the app.config on the fly is not good form (that's what the built-in user settings class is for), so I therefore leave that as an exercise for the programmer.

For purposes of this example, here's the app.config file I'm using:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    
    <appSettings>
        
        <!-- =============================================================================
        The following values are common among all services. Applicabale settings  might 
        be an smtp server address, and things like that. 
        ============================================================================== -->
        <add key="SmtpServer" value="12.345.678.9"/>
        <add key="FromAddress" value="my.address@mydomain.com"/>
        <add key="NotifyAddress" value="notified@notifydomain.com"/>
        <add key="FfmpegPath" value="c:\program files\ffmpeg\ffmpeg.exe"/>
        <add key="DeferValidation" value="false"/>
        <add key="AllowDebugLaunch" value="false"/>

        <!-- =============================================================================
        The following values are unique among all services. Each service might have it's 
        own value for Test1. Values you might want to include here are things like the 
        event log name, a timeout value, service-specific folders, and things like that.
        ============================================================================== -->
        <add key="Service1_EventLogName" value="Service1_Log"/>
        <add key="Service1_UploadPath" value="C:\Upload\Service1"/>
        <add key="Service1_AllowDebugLaunch" value="true"/>

        <add key="Service2_EventLogName" value="Service2_Log"/>
        <add key="Service2_UploadPath" value="C:\Upload\Service2"/>
        <add key="Service2_DeferValidation" value="true"/>
        <add key="Service2_UniqueSetting" value="12345"/>
        
    </appSettings>
    
</configuration>

The AppConfigBase Class

I started off by creating a class that handled reading and validating key values. This keeps a lot of minutia hidden away and goes a long way towards keeping the derived classes much less cluttered. Here's the prototype for the class:

/// <span class="code-SummaryComment"><summary></span>
/// This is the ultimate base class for reading the AppConfig.  It contains 
/// configuration code that tells derived classes how to work, and validation 
/// code for retrieved values.  Because it is derived from IClonable(for the 
/// purpose of cloning itself), it cannot be an abstract class.  If one or 
/// more errors occur during the loading of values, m_errors will not be 
/// empty, so use the Errors property to verify there are no problems before 
/// using the object.
/// <span class="code-SummaryComment"></summary></span>
public class AppConfigBase : ICloneable
{
    #region Icloneable code
    // cloning stuff
    private int baseState;

    //--------------------------------------------------------------------------------
    /// <span class="code-SummaryComment"><summary></span>
    /// The protected constructor used for "clones"
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name="that"></param></span>
    protected AppConfigBase(AppConfigBase that)

    //--------------------------------------------------------------------------------
    /// <span class="code-SummaryComment"><summary></span>
    /// Creates and returns a copy of this object
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><returns></returns></span>
    public virtual object Clone()
    #endregion Icloneable code

    protected bool m_ignoreConfigPrefix = true;
    protected string m_configPrefix = "";
    protected StringBuilder m_errors = new StringBuilder("");

    //--------------------------------------------------------------------------------
    /// <span class="code-SummaryComment"><summary></span>
    /// Get the accumulated errors string
    /// <span class="code-SummaryComment"></summary></span>
    public string Errors { get { return m_errors.ToString(); } }

    //--------------------------------------------------------------------------------
    /// <span class="code-SummaryComment"><summary></span>
    /// Get the application's path
    /// <span class="code-SummaryComment"></summary></span>
    public string ApplicationPath { get { return Environment.CurrentDirectory; } }

    //--------------------------------------------------------------------------------
    /// <span class="code-SummaryComment"><summary></span>
    /// Get the configuration prefix (this can only be set via the class' constructor)
    /// <span class="code-SummaryComment"></summary></span>
    public string ConfigPrefix { get { return m_configPrefix; } }

    //--------------------------------------------------------------------------------
    /// <span class="code-SummaryComment"><summary></span>
    /// Get/Set the value that determines wheher or not to ignore the configuration prefix
    /// <span class="code-SummaryComment"></summary></span>
    public bool IgnoreConfigPrefix
    {
        get { return m_ignoreConfigPrefix; }
        set { m_ignoreConfigPrefix = value; }
    }

    //--------------------------------------------------------------------------------
    /// <span class="code-SummaryComment"><summary></span>
    /// Public constructor
    /// <span class="code-SummaryComment"></summary></span>
    public AppConfigBase()
    {
    }

    //--------------------------------------------------------------------------------
    /// <span class="code-SummaryComment"><summary></span>
    /// Reset the errors string to be empty.
    /// <span class="code-SummaryComment"></summary></span>
    protected void ResetErrors()

    //--------------------------------------------------------------------------------
    /// <span class="code-SummaryComment"><summary></span>
    /// Returns the prefix-adjusted key name. If IgnoreConfigPrefix is false, 
    /// it returns the specified name without any changes
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name="name"></param></span>
    /// <span class="code-SummaryComment"><returns></returns></span>
    protected string KeyName(string name)

    //--------------------------------------------------------------------------------
    /// <span class="code-SummaryComment"><summary></span>
    /// Retrieves the specified value from the app.config file (this method 
    /// simply allows the programmer to do less typing in order to load a 
    /// value from the app.config file).
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name="keyName">The key name of the desired value</param></span>
    /// <span class="code-SummaryComment"><returns>The value retrieved, or an empty string if the key doesn't exist</returns></span>
    protected string GetValue(string key)

    //--------------------------------------------------------------------------------
    /// <span class="code-SummaryComment"><summary></span>
    /// Gets integer values from the app.config file
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name="key">The key name of the desired value</param></span>
    /// <span class="code-SummaryComment"><returns>The value retrieved, or -1 if the key doesn't exist or is null</returns></span>
    protected int GetIntValue(string key)

    //--------------------------------------------------------------------------------
    /// <span class="code-SummaryComment"><summary></span>
    /// Gets double values from the app.config file
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name="key">The key name of the desired value</param></span>
    /// <span class="code-SummaryComment"><returns>The value, or 0.0 if the value isn't found</returns></span>
    protected double GetDoubleValue(string key)

    //--------------------------------------------------------------------------------
    /// <span class="code-SummaryComment"><summary></span>
    /// Gets integer values from the app.config file
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name="key">The key name of the desired value</param></span>
    /// <span class="code-SummaryComment"><returns>True if the value is "true", "yes", or not 0.</returns></span>
    protected bool GetBoolValue(string key)

    //--------------------------------------------------------------------------------
    /// <span class="code-SummaryComment"><summary></span>
    /// If the specified path starts with a tilde, this function combines the 
    /// application path with the specified path, and returns the new combined 
    /// path. Otherwise, the original path is returned.
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name="path">The path to be fixed up</param></span>
    /// <span class="code-SummaryComment"><returns>The fixed up path</returns></span>
    protected string FixupPath(string path)

    //--------------------------------------------------------------------------------
    /// <span class="code-SummaryComment"><summary></span>
    /// Verifies that the path exists.
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name="key">The associated keyname inside the app.config file</param></span>
    /// <span class="code-SummaryComment"><param name="path">The value we extracted from the app.config file</param></span>
    /// <span class="code-SummaryComment"><param name="mustExist">Whether or not the folder MUST exist</param></span>
    /// <span class="code-SummaryComment"><returns>True if the folder exists</returns></span>
    protected bool PathExists(string key, string path, bool mustExist)

    //--------------------------------------------------------------------------------
    /// <span class="code-SummaryComment"><summary></span>
    /// Verifies that the path/file exists.
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name="key">The associated keyname inside the app.config file</param></span>
    /// <span class="code-SummaryComment"><param name="path">The value we extracted from the app.config file</param></span>
    /// <span class="code-SummaryComment"><param name="mustExist">Whether or not the path MUST exist</param></span>
    /// <span class="code-SummaryComment"><returns>True if the path exists</returns></span>
    protected bool FileExists(string key, string path, bool mustExist)

    //--------------------------------------------------------------------------------
    /// <span class="code-SummaryComment"><summary></span>
    /// Verifies that the string value is set to something (not empty).
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name="key">The associated keyname inside the app.config file</param></span>
    /// <span class="code-SummaryComment"><param name="path">The value we extracted from the app.config file</param></span>
    /// <span class="code-SummaryComment"><param name="mustExist">Whether or not the value MUST be set</param></span>
    /// <span class="code-SummaryComment"><returns>True if the value isn't empty</returns></span>
    protected bool ValueIsEmpty(string key, string value, bool mustHaveValue)

    //--------------------------------------------------------------------------------
    /// <span class="code-SummaryComment"><summary></span>
    /// Got this function from MSDN.COM - atempts to ping the specified host. 
    /// Keep in mind that some/most hosts are accessed via a firewall, and 
    /// the firewall may be configured to refuse or not respond to ping 
    /// requests.  As a result, this function might not be very useful.
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name="host">The host name to ping</param></span>
    /// <span class="code-SummaryComment"><returns>True if the host can be pinged</returns></span>
    protected bool PingHost(string host)

    //--------------------------------------------------------------------------------
    /// <span class="code-SummaryComment"><summary></span>
    /// Validates the specified host (or IP address).
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name="key">The key name of the desired value</param></span>
    /// <span class="code-SummaryComment"><param name="value">The IP or host name</param></span>
    /// <span class="code-SummaryComment"><param name="canBeNull">It's okay if this value is null/empty</param></span>
    /// <span class="code-SummaryComment"><returns>True if the ip/host name is correctly formatted</returns></span>
    protected bool ValidHostOrAddress(string key, string value, bool canBeNull)

    //--------------------------------------------------------------------------------
    /// <span class="code-SummaryComment"><summary></span>
    /// Validates the specified semicolon-delimited string of email addresses
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name="key">The key name of the desired value</param></span>
    /// <span class="code-SummaryComment"><param name="value">The email address to be validated</param></span>
    /// <span class="code-SummaryComment"><returns>True if the specified email address is formatted correctly</returns></span>
    protected bool ValidEmailAddress(string key, string value)

    //--------------------------------------------------------------------------------
    /// <span class="code-SummaryComment"><summary></span>
    /// Validates that the specified number is within range
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name="key">The key name of the desired value</param></span>
    /// <span class="code-SummaryComment"><param name="number">The bnumber to be validated</param></span>
    /// <span class="code-SummaryComment"><param name="minNumber">The lowest possible value</param></span>
    /// <span class="code-SummaryComment"><param name="maxNumber">The highest possible value</param></span>
    /// <span class="code-SummaryComment"><returns>True if the specified number is in range</returns></span>
    protected bool NumberInRange(string key, int number, int minNumber, int maxNumber)
}

Values are retrieved from the app.config file with the following functions, with each retrieval being a two-stage process. The class first attempts to load specified key with the configPrefix prepended to it. If a value is not found in the app.config with that key name (prefix + specified name), the function then attempts to load the specified key without a prefix prepended to it. This saves the programmer from having to handle this in derived classes.

//--------------------------------------------------------------------------------
protected string KeyName(string name)
{
    return (IgnoreConfigPrefix) ? name : string.Format("{0}{1}", ConfigPrefix, name);
}

//--------------------------------------------------------------------------------
protected string GetValue(string key)
{
    string value = "";
    // first, we try it with the config prefix 
    if (ConfigurationManager.AppSettings[KeyName(key)] != null)
    {
        value = ConfigurationManager.AppSettings[KeyName(key)].ToString().Trim();
    }
    else
    {
        // the prefixed key didn't exist, so look for the non-prefixed key
        // save the state of the ignore toggle
        bool m_oldIgnore = m_ignoreConfigPrefix;
        // change it to false
        m_ignoreConfigPrefix = true;
        // try to load the key value
        if (ConfigurationManager.AppSettings[KeyName(key)] != null)
        {
            value = ConfigurationManager.AppSettings[KeyName(key)].ToString().Trim();
        }
        // restore the ignore toggle state
        m_ignoreConfigPrefix = m_oldIgnore;
    }
    return value;
}

The only thing you should really need to do in this class is add more GetXXXValue variants and/or validation functions, such as ensuring that an integer value is a power of 2, or an integer value is evenly divisible by 1024.

The AppConfig Class

I use this class to load settings that are common, or that are always "overridden" for the service in question. Most of your setting retrieval and validation should be done here. Here's the class. Note how un-cluttered this class is because of being derived from AppConfigBase. Note also that the settings may be considered invalid because you probably don't have ffmpeg in the specified location.

/// <span class="code-SummaryComment"><summary></span>
/// This class loads common settings.
/// <span class="code-SummaryComment"></summary></span>
public class AppConfig : AppConfigBase, ICloneable
{
    #region IClonable support
    private float derivedState;
    protected AppConfig(AppConfig that)
        : base(that)
    {
        this.derivedState = that.derivedState;
    }

    public override object Clone()
    {
        // Delegate to copy ctor.
        return new AppConfig(this);
    }
    #endregion IClonable support

    #region common settings/properties
    protected string m_smtpServer = "";
    protected string m_fromAddress = "";
    protected string m_notifyAddress = "";
    protected string m_ffmpegPath = "";
    protected bool m_deferValidation = false;
    protected bool m_allowDebugLaunch = false;

    public string SmtpServer { get { return m_smtpServer; } }
    public string FromAddress { get { return m_fromAddress; } }
    public string NotifyAddress { get { return m_notifyAddress; } }
    public string FfmpegPath { get { return m_ffmpegPath; } }
    public bool DeferValidation { get { return m_deferValidation; } }
    public bool AllowDebugLaunch { get { return m_allowDebugLaunch; } }
    #endregion common settings/properties


    //--------------------------------------------------------------------------------
    public AppConfig()
    {
    }

    //--------------------------------------------------------------------------------
    /// <span class="code-SummaryComment"><summary></span>
    /// Loads the common settings
    /// <span class="code-SummaryComment"></summary></span>
    public virtual void Reload()
    {
        m_smtpServer = GetValue("SmtpServer");
        m_fromAddress = GetValue("FromAddress");
        m_notifyAddress = GetValue("NotifyAddress");
        m_ffmpegPath = GetValue("FfmpegPath");
        m_deferValidation = GetBoolValue("DeferValidation");
        // you can only debug a service if debug.Launch is used - this settings 
        // allows you to disable it without re-compiling the code
        m_allowDebugLaunch = GetBoolValue("AllowDebugLaunch");
        if (!DeferValidation)
        {
            ValidateData();
        }
    }

    //--------------------------------------------------------------------------------
    /// <span class="code-SummaryComment"><summary></span>
    /// Validates the common settings
    /// <span class="code-SummaryComment"></summary></span>
    public virtual void ValidateData()
    {
        this.ValidHostOrAddress("SmtpServer", m_smtpServer, false);
        this.ValidEmailAddress("FromAddress", m_fromAddress);
        this.ValidEmailAddress("NotifyAddress", m_notifyAddress);
        this.PathExists("FfmpegPath", FfmpegPath, true);
    }
}

The AppConfigService1 Class

This class is derived from AppConfig, and demonstrates the loading of service-specific versions of certain keys. Notice that the constructor accepts a string that represents the prefix used for the service's settings. Also, notice that this class calls the base class Reload() method (which calls its own Validate() method), and then loads/validates settings specific to the service.

public class AppConfigService1 : AppConfig, ICloneable
{
    #region IClonable support
    private float derivedState;
    protected AppConfigService1(AppConfigService1 that) : base(that)
    {
        this.derivedState = that.derivedState;
    }
    public override object Clone()
    {
        // Delegate to copy ctor.
        return new AppConfigService1(this);
    }
    #endregion IClonable support

    #region service-specific settings/properties
    protected string m_logName = "";
    protected string m_uploadPath = "";

    public string LogName { get { return m_logName; } }
    public string UploadPath { get { return m_uploadPath; } }
    #endregion common settings/properties

    //--------------------------------------------------------------------------------
    /// <span class="code-SummaryComment"><summary></span>
    /// Public constructor
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name="configPrefix">The prefix to be used to load service-specific settings</param></span>
    public AppConfigService1(string configPrefix)
    {
        IgnoreConfigPrefix = false;
        m_configPrefix = configPrefix;
        Reload();
    }

    //--------------------------------------------------------------------------------
    /// <span class="code-SummaryComment"><summary></span>
    /// Loads the service-specific settings
    /// <span class="code-SummaryComment"></summary></span>
    public override void Reload()
    {
        // load common settings
        base.Reload();

        // loud our service-specific settings
        m_logName = GetValue("LogName");
        m_uploadPath = GetValue("UploadPath");

        // validate
        if (!this.DeferValidation)
        {
            ValidateData();
        }
    }

    //--------------------------------------------------------------------------------
    /// <span class="code-SummaryComment"><summary></span>
    /// Validates the service-specific settings
    /// <span class="code-SummaryComment"></summary></span>
    public override void ValidateData()
    {
        ValueIsEmpty("LogName", m_logName, true);
        PathExists("UploadPath", m_uploadPath, true);
    }
}

The AppConfigService2 Class

Like AppConfigService1, this class is derived from AppConfig. Unlike AppConfigService1, this class loads an additional setting that is only used by Service2. While it's not necessary to prefix unique values, I think that it's important to adhere to the prefixed key naming convention when adding settings that are used only by one service. Programmers performing maintenance on your code in the future will certainly appreciate it (whether they admit it or not).

public class AppConfigService2 : AppConfig, ICloneable
{
    #region IClonable support
    private float derivedState;
    protected AppConfigService2(AppConfigService2 that) : base(that)
    {
        this.derivedState = that.derivedState;
    }
    public override object Clone()
    {
        // Delegate to copy ctor.
        return new AppConfigService2(this);
    }
    #endregion IClonable support

    #region service-specific settings/properties
    protected string m_logName = "";
    protected string m_uploadPath = "";

    public string LogName { get { return m_logName; } }
    public string UploadPath { get { return m_uploadPath; } }
    #endregion common settings/properties

    protected int m_uniqueSetting = -1;

    public int UniqueSetting { get { return m_uniqueSetting; } }

    //--------------------------------------------------------------------------------
    /// <span class="code-SummaryComment"><summary></span>
    /// Public constructor
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name="configPrefix">The prefix to be used to load service-specific settings</param></span>
    public AppConfigService2(string configPrefix)
    {
        IgnoreConfigPrefix = false;
        m_configPrefix = configPrefix;
        Reload();
    }

    //--------------------------------------------------------------------------------
    /// <span class="code-SummaryComment"><summary></span>
    /// Loads the service-specific settings
    /// <span class="code-SummaryComment"></summary></span>
    public override void Reload()
    {
        // load common settings
        base.Reload();

        // loud our service-specific settings
        m_logName = GetValue("LogName");
        m_uploadPath = GetValue("UploadPath");

        // load our unique setting for this service
        m_uniqueSetting = GetIntValue("UniqueSetting");

        // validate
        if (!this.DeferValidation)
        {
            ValidateData();
        }
    }

    //--------------------------------------------------------------------------------
    /// <span class="code-SummaryComment"><summary></span>
    /// Validates the service-specific settings
    /// <span class="code-SummaryComment"></summary></span>
    public override void ValidateData()
    {
        ValueIsEmpty("LogName", m_logName, true);
        PathExists("UploadPath", m_uploadPath, true);
        NumberInRange("UniqueSetting", m_uniqueSetting, 1, 65536);
    }
}

IConeable Code

I needed a deep copy capability for these classes due to the way we code our services, and I found this code on the net. For more info about it go to the link provided. The short version is that it utilizes a copy constructor to create a deep copy of the original object.

Using The Classes

Simply instantiate the service-specific classes with the appropriate prefix specification, and you're off to the races. Using this code couldn't be much simpler.

    AppConfigService1 ac1 = new AppConfigService1("Service1_");
    AppConfigService2 ac2 = new AppConfigService2("Service2_");

The sample application provided in the source code is just a console application that instantiates these classes, and writes the settings to the console (see the screen shot at the beginning of this article).

Visual Studio Complaint

When you add an applocation configuration file to your project, why doesn't the IDE automatically add a reference for System.Configuration? It seems to me that it would be a simple thing to do and I can't think of a single reason why you wouldn't want it to happen. This is just another example of Microsoft turning off their brains with regards to making our developing lives just a little bit easier. Granted, this is a very minor issue, but still...

In Closing

Like I said before - there are more .Net-centric solutions that can be pursued, and these solutions involve using section groups and sections, and (like this solution) a bit of additional coding. This is merely the way I do it. Chances are pretty good that if you don't often write Windows services, you will probably never have cause to write a service that implements multiple services. However, this set of classes can be used elsewhere, and at least the base class can be used to support your own app.config loading needs. You could canibalize (and adjust) the AppConfigBase class and just use it's validation functions in combination with the section group/section paradigm.

Lastly, this project was written with Visual Studio 2008, but the programmer (you) shouldn't have any problem at all making it compile under VS2005. Simply copy the appropriate files to your VS2005 project and go for it (don't forget to change the namespace to something appropriate for your application).

History

17 May 2008: Original article posted.

License

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

Share

About the Author

John Simmons / outlaw programmer
Software Developer (Senior)
United States United States
I've been paid as a programmer since 1982 with experience in Pascal, and C++ (both self-taught), and began writing Windows programs in 1991 using Visual C++ and MFC. In the 2nd half of 2007, I started writing C# Windows Forms and ASP.Net applications, and have since done WPF, Silverlight, WCF, web services, and Windows services.
 
My weakest point is that my moments of clarity are too brief to hold a meaningful conversation that requires more than 30 seconds to complete. Thankfully, grunts of agreement are all that is required to conduct most discussions without committing to any particular belief system.

Comments and Discussions

 
GeneralBy The Way PinmvpJohn Simmons / outlaw programmer12-Nov-08 10:17 
QuestionWould this be possible? PinprotectorMarc Clifton17-May-08 11:04 
AnswerRe: Would this be possible? PinmvpJohn Simmons / outlaw programmer17-May-08 11:37 
GeneralRe: Would this be possible? PinprotectorMarc Clifton17-May-08 11:44 
GeneralRe: Would this be possible? [modified] PinmvpJohn Simmons / outlaw programmer17-May-08 11:49 
GeneralRe: Would this be possible? PinprotectorMarc Clifton18-May-08 15:40 
GeneralNice one there, 5 from me PinmvpSacha Barber17-May-08 3:59 
GeneralRe: Nice one there, 5 from me PinmvpJohn Simmons / outlaw programmer17-May-08 4:03 
GeneralRe: Nice one there, 5 from me PinmvpSacha Barber17-May-08 4:35 

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
Web01 | 2.8.140921.1 | Last Updated 17 May 2008
Article Copyright 2008 by John Simmons / outlaw programmer
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid