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

A Simple C# Property System

, 31 Jan 2013
Rate this:
Please Sign up or sign in to vote.
The following article demonstrates a C# implementation of a simple property system.

Contents

  1. Introduction
  2. Background
  3. Interfaces Overview
    1. Property System Interface 
    2. Property Definitions Interface
    3. Property Definition Interface
    4. Property Bag Interface 
  4. Where does the magic happen
  5. Using the code to manage a property system on the fly
    1. Create a system instance
    2. Create property definitions for later use by the property system
    3. Assign the property definitions to the property system instance
    4. Create a property bag instance from the property system instance
    5. Use the property bag instance
  6. Using the code to build a bag-based object
    1. Define property id's 
    2. Define a specialised property system 
    3. Create an example object definition 
    4. Create an example system manager 
  7. Limitations 
  8. Points of Interest 
  9. References 
  10. History 

Introduction

This property system example implementation is basically a property bag with some management to override values with functions and the potential to be used to compose an entity system with inheritance, but it can also be used as a message bus, a common base for components and much more.

Background

Property Systems, Object Systems, Entity Systems or whatever else such sort of "functionality and structure" can be called are quite a handy way to work with heterogenous data and to separate concerns.

I've read quite a lot about the subject, and before I try to repeat what others said more clearly than I probably ever could, i recommend studying the references.

However, it seems like the world agrees on the demand for something which can replace huge class hierarchies and even microsoft provides quite much around the topic; DependencyProperty and the new dynamic keyword in C# for example.

The example provided here is a partial replacement for the dynamic keyword and some related stuff and it has the potential to be a partial replacement for dependency properties. It's not a component system, but it can be used to create one.

The article focuses on building a property system which is able to store any kind of datatype and which provides the possibility to "override" the concrete value of a certain property with a function returning such. This allows to easily setup relationships between different properties or accross property systems which might be the main advantage compared to other property bag implementations.

Avoiding the use of the new dynamic keyword was a design goal as well as providing a better base for dependency property like structures. XML De-/serialization isn't part of this example and has been completely left away as it's with thread safety and performance issues. However, some of these might be covered by further articles.

The property systems can be used directly or encapsulated in dedicated classes. They're handy to compose generic objects, build component containers, serve as bus systems and for much more.

The provided code includes a testcase for their direct use and one which shows an example of a generic object.

Although there's the possibility to provide event references, these are not (yet) covered by the article, but included in the source code. A tip/trick where their approach is explained can be found here: www.codeproject.com/Tips/454172/Referencing-events-in-Csharp-using-interfaces-A-si [^]. 

Hope you enjoy.

Interfaces Overview

Following is a short overview of the over all major interfaces of the property system:

Property System Interface

The property system defines a key type (K), provides a factory method to create bags and allows to work with definitions:

public interface IPropertySystem<K> where K : IComparable, IFormattable, IConvertible, IComparable<K>, IEquatable<K>

{
  IPropertyBag<K> CreateBag();

  event Action<IPropertyDef<K> AfterDefinitionAdded;
  event Action<IPropertyDef<K> AfterDefinitionRemoved;

  IPropertyDef<K> GetDef(K propId);
  void SetDefs(IPropertyDefs<K> defs);
}

Property Definitions Interface

The property definitions defines the same key type as the system, provides a factory method to create definitions and allows to work with definitions:

public interface IPropertyDefs<K> where K : IComparable, IFormattable, 
  IConvertible, IComparable<K>, IEquatable<K>

{
  // factory
  IPropertyDef<K> CreateDef(K id, Type type);

  event Action<IPropertyDef<K> AfterDefinitionAdded;
  event Action<IPropertyDef<K> AfterDefinitionRemoved;

  void SetDefs(List<IPropertyDef<K> newDefs);
  void AddDefs(List<IPropertyDef<K> newDefs);
  void AddDef(IPropertyDef<K> newDef);
  void RemoveDef(K propertyId);
  void RemoveDef(IPropertyDef<K> def);

  IPropertyDef<K> GetDef(K propertyId);
  List<IPropertyDef<K> Defs { get; }
}

Property Definition Interface

The property definition defines the same key type as the system and is a container for Id and ValueType values:

public interface IPropertyDef<K> where K : IComparable, IFormattable, IConvertible, IComparable<K>, IEquatable<K>

{
  K Id { get; set; }
  Type ValueType { get; set; }
}

Property Bag Interface

The property bag class defines the same key type as the system and allows to create/modify/delete property values:

public interface IPropertyBag<K> where K: IComparable, 
  IFormattable, IConvertible, IComparable<K>, IEquatable<K>

{
  event Func<K, object, bool> BeforeFuncAdd;
  event Action<K, object> AfterFuncAdd;
  event Action<K, object> AfterFuncAddFailed;
  	
  event Func<K, object, bool> BeforeFuncSet;
  event Action<K, object> AfterFuncSet;
  event Action<K, object> AfterFuncSetFailed;
  	
  event Func<K, object, bool> BeforeValueAdd;
  event Action<K, object> AfterValueAdd;
  event Action<K, object> AfterValueAddFailed;
  	
  event Func<K, object, bool> BeforeValueSet;
  event Action<K, object> AfterValueSet;
  event Action<K, object> AfterValueSetFailed;
  	
  event Func<K, bool> BeforeRemove;
  event Action<K> AfterRemove;

  bool TryGet(K propId, out object retValue);
  bool TryGetFunc(K propId, out object retValue);
  bool TryGetValue<T>(K propId, out T retValue);
  T GetValue<T>(K propId, T defaultValue);
  T GetValue<T>(K propId, Action<T> nonNullAction);
  T GetValue<T>(K propId, Action<T> nonNullAction, Action nullAction);
  R GetValue<T, R>(K propId, Func<T, R> nonNullFunc, R defaultValue);
  R GetValue<T, R>(K propId, Func<T, R> nonNullFunc, Func<R> nullFunc);

  void Set(K propId, object value);
  void SetFunc(K propId, object value);
  void SetValue<T>(K propId, T value);

  bool TryRemove(K propId);
}

Where does the magic happen

Following, some of the more interresting code fragments of the system are presented.

In fact, there aren't much. Most of the magic happens in the PropertyBag<K>'s TryGetValue<T> method.

The frame is straight forward layed out by some case determinations:

public bool TryGetValue<T>(K propId, out T retValue)
{
  if (properties.ContainsKey(propId))
  {
    object p = properties[propId];

    if (p == null)
    {
      // todo: handle property null value
    }
    else
    {
      // todo: handle property non-null value
    }
  }
  else
  {
    // todo: handle property not set
  }
}

Having no property set is the easiest case - returning a default value for the type and false as return seems sufficient:

  // handle property not set
  retValue = default(T);
  return false;

If the stored value for the property is null, we have to check if the properties type is not a value type and raise an exception if needed, because they don't like the null:

  // handle property null value
  Type Ttype = typeof(T);
  if (!Ttype.IsValueType)
  {
    retValue = default(T);
    return true;
  }
  else
    throw new Exception(string.Format("Value types don't support null"));

The last step and maybe the most interresting part (although, it's no rocket science) is the type magic done to determine if the property is overridden by a function.

The approach is simple: The property definition in the system tells us what type to expect. If the contained type differs from the expected one, we just check if it's a function returning the expected type. If not, we're screwed and throw an exception:

  // handle property non-null value
  Type t = p.GetType();
  IPropertyDef<K> pd = system.GetDef(propId);
  Type vt = pd.ValueType;
  bool b = vt.IsAssignableFrom(t);

  if (b) // same type ^= no func
    retValue = (T)p;
  else
  {
    Func<T> f = p as Func<T>

    if (f == null)
      throw new Exception(string.Format("expected Func<{0}> but got {1}", typeof(T).ToString(), t.ToString()));
    else
      retValue = f();
  }

  return true;

Finally, the whole code for the method:

public bool TryGetValue<T>(K propId, out T retValue)
{
  if (properties.ContainsKey(propId))
  {
    object p = properties[propId];

    if (p == null)
    {
      Type Ttype = typeof(T);
      if (!Ttype.IsValueType)
      {
        retValue = default(T);
        return true;
      }
      else
        throw new Exception(string.Format("Value types don't support null"));
    }
    else
    {
      Type t = p.GetType();
      IPropertyDef<K> pd = system.GetDef(propId);
      Type vt = pd.ValueType;
      bool b = vt.IsAssignableFrom(t);

      if (b) // same type ^= no func
        retValue = (T)p;
      else
      {
        Func<T> f = p as Func<T>

        if (f == null)
          throw new Exception(string.Format("expected Func<{0}> but got {1}", typeof(T).ToString(), t.ToString()));
        else
          retValue = f();
      }

      return true;
    }
  }
  else
  {
    retValue = default(T);
    return false;
  }
}

Using the code to manage a property system on the fly

The following paragraphs show how to create a property system on the fly and some basic use cases:

Create a system instance

A property system instance needs at least a key type. Although the property definitions can be set up before constructing the system instance, it makes sense to split system and definition creation for this example.

  // Create an instance of a property system with a string key type
  PropertySystem<string> ps = new PropertySystem<string>();

Create property definitions for later use by the property system

For the property system being able to distinguish between values and functions returning values, it needs proper definitions set up:

  // Create a container for the definitions
  PropertyDefs<string> pd = new PropertyDefs<string>();
  // and add some definitions; here an integer property named (= a key with the value of) "test-int"

  pd.AddDef(pd.CreateDef("test-int", typeof(int)));
  // add a new definition for a parameterless function returning a boolean value and which is named "a func returning a boolean"
  pd.AddDef(pd.CreateDef("a func returning a boolean", typeof(Func<bool>)));

Assign the property definitions to the property system instance

The property system instance needs to know about its possible properties because the bag it creates uses the information to distinguish between value- and function types:

  // assign the definitions to the property system instance
  ps.SetDefs(pd);

Create a property bag instance from the property system instance

As the property system may be seen as a class definition (although, the comparison is misleading), the property bag may be seen as an instance [of the property system seen as a class] (although, the comparison is misleading as well):

  // create a bag
  PropertyBag<string> pb = ps.CreateBag() as PropertyBag<string>;

Use the property bag instance

Now that everything is set up, we can start using it.

Because the bag is still empty, we first fill in some data:

  pb.SetValue("test-int", 42);
  pb.SetValue("a func returning a boolean", new Func<bool>(() => true));

Getting the data back from the bag is very easy. Thanks to generic type inference, we don't even have to specify the return types explicitely if we're storing them directly:

  bool ret;

  int iValue;
  ret = pb.TryGetValue("test-int", out iValue);

  Func<bool> fBool;
  ret = pb.TryGetValue("a func returning a boolean", out fBool);

However, there are some convenience methods for a more fluent use.

If it's possible to provide a default value, there's no need for a return value indicating the success of the get operation:

  iValue = pb.GetValue("test-int", 0815);

Chaining through functions is also possible, however, the compiler then needs a type hint:

  pb.GetValue<int>("test-int", v => Debug.WriteLine(string.Format("successfully got {0}", v)), () => Debug.WriteLine("GetValue failed"));

Transformations are also possible; here, our int is returned as a string:

  pb.GetValue<int, string>("test-int", v => v.ToString(), () => "GetValue failed");

Remember that we defined "test-int" as typeof(int) and "a func returning a boolean" as typeof(Func<bool>) and that there's the possibility to override any property value with a corresponding function returning the type we defined for the property:

  pb.SetValue("test-int", new Func<int>(() => 42));
  pb.SetValue("a func returning a boolean", new Func<Func<bool>>(() => new Func<bool>(() => true)));

Getting the values remains the same as before because the interface is transparent. However, there may be occasions when you know that there's a function behind a value property. To directly access it, there's another method in the bag:

  object o;
  ret = pb.TryGetFunc("test-int", out o);
  ret = pb.TryGetFunc("a func returning a boolean", out o);

Using the code to create a property-bag based object

The following paragraphs show how to create a property-bag based object.

Because the example is so much used and widely understood, we're going to create a generic definition of amphibian vehicles.

For sake of simplicity, there will be only one method: Accelerate.

Define property id's

Even if the key type of the system is string, it sometimes is very handy to have constants ready to avoid polluting the source code with magic numbers.

Because we're using integers as keys in the example, it's no question for me whether to define proper constants for the property id's or not.

Since they're used quite often, it could make sense to keep the container names short:

abstract class Ipsc
{
  // properties
  public const int Id = 0;
  public const int MaxDriveSpeed = 1;
  public const int MaxFlySpeed = 2;
  public const int MaxSwimSpeed = 3;
  public const int CurrentMedium = 4;
  public const int CurrentSpeed = 5;
  // actions / functions
  public const int Accelerate = 10;
  // todo: events
}

Define a specialised property system

The system definition can be a derivation of the original PropertySystem. We're going to approach it with aggregation and just implement the interface IPropertySystem. For performance reasons, the key type is defined as integer.

class IntKeyPropertySystem : IPropertySystem<int>
{
  IPropertySystem<int> propSys;

  public IntKeyPropertySystem()
    : base()
  {
    // todo: create and define the property system
  }
  
  // todo: provide interface implementations
}

The next step is to implement all needed interfaces. Proxying them to the base object is sufficient for this example:

  // interface implementations
  public event Action<IPropertyDef<int>> AfterDefinitionAdded;
  public event Action<IPropertyDef<int>> AfterDefinitionRemoved;

  public void SetDefs(IPropertyDefs<int> defs)
  {
    propSys.SetDefs(defs);
  }
  public IPropertyBag<int> CreateBag()
  {
    return propSys.CreateBag();
  }
  public IPropertyDef<int> GetDef(int propId)
  {
    return propSys.GetDef(propId);
  }

We may not forget to create the system and setup the event routes:

public IntKeyPropertySystem() : IPropertySystem<int>
  : base()
{
  // create the property system
  PropertySystemFactory psFactory = new PropertySystemFactory();
  propSys = psFactory.Create<int>();
  	
  // todo: define properties

  // properly route the events
  propSys.AfterDefinitionAdded += new Action<IPropertyDef<int>>(propSys_AfterDefinitionAdded);
  propSys.AfterDefinitionRemoved += new Action<IPropertyDef<int>>(propSys_AfterDefinitionRemoved);
}

void propSys_AfterDefinitionAdded(IPropertyDef<int> def)
{
  if (AfterDefinitionAdded != null)
    AfterDefinitionAdded(def);
}
void propSys_AfterDefinitionRemoved(IPropertyDef<int> def)
{
  if (AfterDefinitionRemoved != null)
    AfterDefinitionRemoved(def);
}

Finally, we define the available properties and assign them to the system:

  // define properties
  IPropertyDefs<int> defs = psFactory.CreateDefs<int>();
  defs.SetDefs(new List<IPropertyDef<int>>{
    { defs.CreateDef(Ipsc.Id, typeof(int)) },
    { defs.CreateDef(Ipsc.MaxDriveSpeed, typeof(double)) },
    { defs.CreateDef(Ipsc.MaxFlySpeed, typeof(double)) },
    { defs.CreateDef(Ipsc.MaxSwimSpeed, typeof(double)) },
    { defs.CreateDef(Ipsc.CurrentMedium, typeof(Medium)) },
    { defs.CreateDef(Ipsc.CurrentSpeed, typeof(double)) },

    { defs.CreateDef(Ipsc.Accelerate, typeof(Action<double>)) }
  	
    // todo: define event properties
    });
  propSys.SetDefs(defs);

Create an example object definition

The example object has a reference to the shared bag, a global unique object-id and provides some proxified accessors:

The shared bag is simply set up during construction:

class ExampleObject
{
  IPropertyBag<int> pbag;
  // todo: add id management

  public ExampleObject(IPropertyBag<int> pbag)
  {
    this.pbag = pbag;
    // todo: add id management
  }

  // todo: add accessor methods
}

The UID management is also held simple and at this time just here for reference and easier debugging; however, it often seems to make sense for several reasons to have an object-uid:

class ExampleObject
{
  IPropertyBag<int> pbag;
  // id management
  static int nextId = 0;

  public ExampleObject(IPropertyBag<int> pbag)
  {
    this.pbag = pbag;

    // id management
    this.pbag.SetValue(Ipsc.Id, nextId);
    ++nextId;
  }

  // todo: add accessor methods
}

Finally we define the accessor methods. They may completely differ from the ones from the bag, but in the easiest case, we can just proxify them:

class ExampleObject
{
  static int nextId = 0;
  IPropertyBag<int> pbag;

  public ExampleObject(IPropertyBag<int> pbag)
  {
      this.pbag = pbag;

      this.pbag.SetValue(Ipsc.Id, nextId);
      ++nextId;
  }

  // proxies
  public bool TryGetFunc(int propId, out object retValue)
  {
      return this.pbag.TryGetFunc(propId, out retValue);
  }
  public bool TryGetValue<T>(int propId, out T retValue)
  {
      return this.pbag.TryGetValue<T>(propId, out retValue);
  }
  public T GetValue<T>(int propId, T defaultValue)
  {
      return this.pbag.GetValue<T>(propId, defaultValue);
  }
  public T GetValue<T>(int propId, Action<T> nonNullAction)
  {
      return this.pbag.GetValue<T>(propId, nonNullAction);
  }
  public T GetValue<T>(int propId, Action<T> nonNullAction, Action nullAction)
  {
      return this.pbag.GetValue<T>(propId, nonNullAction, nullAction);
  }
  public R GetValue<T, R>(int propId, Func<T, R> nonNullFunc, R defaultValue)
  {
      return this.pbag.GetValue<T, R>(propId, nonNullFunc, defaultValue);
  }
  public R GetValue<T, R>(int propId, Func<T, R> nonNullFunc, Func<R> nullFunc)
  {
      return this.pbag.GetValue<T, R>(propId, nonNullFunc, nullFunc);
  }

  public void Set(int propId, object value)
  {
      this.pbag.Set(propId, value);
  }
  public void SetFunc(int propId, object value)
  {
      this.pbag.SetFunc(propId, value);
  }
  public void SetValue<T>(int propId, T value)
  {
      this.pbag.SetValue<T>(propId, value);
  }

  public bool TryRemove(int propId)
  {
      return this.pbag.TryRemove(propId);
  }
}

Create an example system manager

So far, we've defined the property system, its properties and the example object. But we still left out the whole logic.

To keep it simple, all logic which is left is put together in one class which serves also as a container for the test definition:

class TestIntKeyPropertySystem
{
  public void Run()
  {
    // todo
  }
}

Creating the system and some example object instances is very easy:

  public void Run()
  {
    // create an instance of our test property system
    IntKeyPropertySystem ikps = new IntKeyPropertySystem();

    // create two example objects, each with its own property bag
    ExampleObject eo1 = new ExampleObject(ikps.CreateBag());
    ExampleObject eo2 = new ExampleObject(ikps.CreateBag());

    // todo: init the properties of the two example objects
    // todo: execute test(s)
  }

Setting property values is also quite painless:

  public void Run()
  {
    // [...]
    
    // init the properties of the two example objects
    // the approach for this example is to route everything through this class
    const double maxDriveSpeedEo1 = 100;
    const double maxSwimSpeed = 40;

    eo1.SetValue(Ipsc.MaxDriveSpeed, maxDriveSpeedEo1);
    eo1.SetValue(Ipsc.MaxFlySpeed, 300);
    eo1.SetValue(Ipsc.MaxSwimSpeed, maxSwimSpeed);
    eo1.SetValue(Ipsc.CurrentMedium, Medium.Ground);
    eo1.SetValue(Ipsc.CurrentSpeed, 0);

    eo2.SetValue(Ipsc.MaxDriveSpeed, 90);
    eo2.SetValue(Ipsc.MaxFlySpeed, 250);
    // we define the MaxSwimSpeed for the example object 2 as dependent on the swim speed of the example object 1 
    // with a default of 50 for the case when the example object 1 doesn't define its maximum swim speed
    eo2.SetFunc(Ipsc.MaxSwimSpeed, new Func<double>(() => eo1.GetValue(Ipsc.MaxSwimSpeed, 50.0)));
    eo2.SetValue(Ipsc.CurrentMedium, Medium.Water);
    eo2.SetValue(Ipsc.CurrentSpeed, 0);

    // todo: define "acceleration"

  }

The only "real" method of our "objects" is Accelerate:

  public void Run()
  {
    // [...]
    
    // define a shared acceleration function and "inject" the object context information
    // one possibility to do so is the one below, where the context is added by defining lambdas appropriately
    eo1.SetValue<Action<double>>(Ipsc.Accelerate, s => Accelerate(eo1, s));
    eo2.SetValue<Action<double>>(Ipsc.Accelerate, s => Accelerate(eo2, s));
  }

Of course, we have to implement the Accelerate method:

  void Accelerate(ExampleObject eo, double speed)
  {
    Medium medium;
    if (!eo.TryGetValue(Ipsc.CurrentMedium, out medium))
      return;

    int prop;
    switch (medium)
    {
      case Medium.Ground:
        prop = Ipsc.MaxDriveSpeed;
        break;
      case Medium.Air:
        prop = Ipsc.MaxFlySpeed;
        break;
      case Medium.Water:
        prop = Ipsc.MaxSwimSpeed;
        break;
      default:
        return;
    }

    // todo: try to ask if the speed change may happen if needed and return if not
    // if we've got a max speed value, limit the new speed accordingly
    double maxSpeed;
    if (eo.TryGetValue(prop, out maxSpeed))
      speed = Math.Min(speed, maxSpeed);

    eo.SetValue(Ipsc.CurrentSpeed, speed);

    // todo: try to inform about the new speed if needed
  }

Limitations

Because SetValue raises the value-changed-event, there is no possibility yet to subscribe for changes of properties which are overridden with functions.

Points of Interest

The presented approach to a property system is only one out of many and is not meant to be used in productive environments yet.

Static type checking is a very powerful mechanism and doing it at runtime obviously raises some performance and safety issues.

Distinguishing between value properties and their function equivalents depends on runtime type information which might be replaced by either putting the info to the properties or to the system. This would also be cool because it would allow to remove the back-reference from the bag to the system. 

Readonly properties are completely left out from the implementation, however, they could be added quite easily, for example by replacing the object container by a more clever one which provides a property for it, or by delegating the management to a specialized bag which keeps such information differently - for example by using separate lists for the properties. 

 

References

  1. stackoverflow/Tomas Vana: Implementation of a general-purpose object structure (property bag) ^
  2. gamedev.net/JTippetts: Entity system ^
  3. west-wind.com/Rick Strahl's Web Log: An Xml Serializable PropertyBag Dictionary Class for .NET ^
  4. cowboyprogramming/Mick West: Evolve Your Hierarchy ^
  5. codeproject.com/Graham Harrison: Implementing a PropertyBag in C# ^
  6. www.academia.edu/401854/A_Generic_Data_Structure_for_An_Architectural_Design_Application ^

History 

  • 2013/01/31: Added a link to the "event referencing" tip/trick, added references link to contents and fixed some spellings
  • 2013/01/24: Added a reference
  • 2012/08/31: Added limitations and an explanation of the implementation 
  • 2012/08/29: First version submitted

License

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

Share

About the Author

zaphoed
Software Developer
Switzerland Switzerland
experienced in asm/c/c++/c#. Less experienced in Python, Lua, Shell and stuff like ASP, PHP, HTML ...
coding for windows, macOS, iOS and linux
just4fun: ogre3d

Comments and Discussions

 
General[My vote of 2] Ugly design Pinmemberabdurahman ibn hattab25-Jan-13 23:04 
GeneralRe: [My vote of 2] Ugly design Pinmemberzaphoed30-Jan-13 0:01 
QuestionWhy reinvent the wheel? PinmemberFatCatProgrammer31-Aug-12 7:46 
AnswerRe: Why reinvent the wheel? Pinmemberrobertw0191-Sep-12 16:11 
AnswerRe: Why reinvent the wheel? Pinmemberzaphoed2-Sep-12 23:05 
GeneralRe: Why reinvent the wheel? [modified] PinmemberFatCatProgrammer3-Sep-12 7:05 
GeneralRe: Why reinvent the wheel? Pinmemberzaphoed4-Sep-12 0:48 
GeneralRe: Why reinvent the wheel? PinmemberFatCatProgrammer4-Sep-12 4:57 
GeneralRe: Why reinvent the wheel? Pinmemberzaphoed4-Sep-12 5:24 
GeneralRe: Why reinvent the wheel? PinmemberFatCatProgrammer4-Sep-12 5:32 
GeneralRe: Why reinvent the wheel? Pinmemberzaphoed4-Sep-12 6:06 
GeneralRe: Why reinvent the wheel? PinmemberFatCatProgrammer4-Sep-12 6:55 
GeneralRe: Why reinvent the wheel? Pinmemberzaphoed4-Sep-12 9:28 
GeneralRe: Why reinvent the wheel? Pinmemberzaphoed20-Sep-12 5:24 
GeneralRe: Why reinvent the wheel? PinmemberFatCatProgrammer20-Sep-12 17:13 

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

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

| Advertise | Privacy | Mobile
Web04 | 2.8.140921.1 | Last Updated 31 Jan 2013
Article Copyright 2012 by zaphoed
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid