Click here to Skip to main content
15,868,141 members
Articles / Programming Languages / C#
Article

Implementing a PropertyBag in C#

Rate me:
Please Sign up or sign in to vote.
4.86/5 (23 votes)
25 Nov 200514 min read 209.4K   4.2K   119   22
Designing and implementing a PropertyBag in C#.

Introduction

This article details the design, coding, and documentation for a PropertyBag class in C#.

When classes are designed, they are given the properties that the designer envisages the clients of that class require. For example, an Address class might need PostCode and StreetName properties which are designed into the class.

It is common for classes to be used outside of the scope intended by the designer. For instance, our hypothetical Address class starts being used in the U.S. arm of the business, when the designer did not envisage any non-U.K. clients. As a result, the class now needs a ZipCode property.

On one hand, the Address class code could be extended, the assembly recompiled, and redistributed. On the other hand, if the Address class exposes a PropertyBag object, then the consumers of the class can assign ad-hoc properties to the PropertyBag, thus removing the need for potentially costly coding, compilation, and distribution.

Requirements of the PropertyBag class

Getting and setting properties

The main requirements of the PropertyBag are to enable client classes to add new properties, assign and re-assign values to those properties, and read the current values out of the properties.

Ease of use

A secondary feature is to design and implement the class in such a way as to make the class interface very intuitive, and easy to use. To achieve this, an indexer is used in the PropertyBag class when accessing the Property class.

Type-safe collections were considered, but a decision was taken to avoid over-complicating the code; type-safe collections will be the subject of a future article instead.

Inner classes were used to simplify the PropertyBag interface, as will be demonstrated further in this article.

Deal with the existing properties intuitively

Another feature is to take account of the "real" properties of the class that contains the PropertyBag.

For instance, if the parent class is the Address class (which already describes a genuine property called Line1), and a client of the Address class attempts to add a PostCode property to the Address class' PropertyBag, then the PropertyBag's PostCode property will refer to the "real" property in the Address class, and not an ad-hoc property with a duplicated name in the PropertyBag.

Use of reflection within the PropertyBag class will enable this requirement to be implemented.

Implement updating and updated events on individual properties

The properties within the PropertyBag class need to implement events such that an event subscriber can be notified when a particular property within the PropertyBag has been updated. The events to be implemented are Updating and Updated. The Updating event will include a mechanism for signaling back to the property that generated the event that the update is to be cancelled, enabling event handlers to validate the proposed new value, and reject the new value if it fails validation.

The article assumes that the reader has a good understanding of implementing events and delegates in C#. This topic is covered in the article Using Events and Delegates in C#.

For instance, if the PropertyBag in the Address class can be used by a client to add a new ZipCode property. The class that added the ZipCode property can subscribe to the Updating event, and reject all new values that are non-numeric.

Serializability

The PropertyBag and Property objects need to be serializable such that, provided that the owner object (the Address class in this example) is serializable, then the PropertyBag and Property objects will also serialize.

Thread safety

The final requirement is for the PropertyBag class to be thread-safe. Implementing events forces a designer to think about scenarios where those events may be consumed. Event handlers are useful within single threaded applications, but are often even more useful in multi-threaded applications, with a change to a property value in one thread, raising an event in another (i.e. the tree view and list views in Windows Explorer).

The writing of properties, and the reading of properties that can be written within the PropertyBag implementation must implement locking to ensure thread safety.

Conclusion

The PropertyBag class is a useful utility to include in general purpose classes to provide support for the introduction of ad-hoc properties during post-design. Its implementation will practically demonstrate indexers, inner classes, reflection, events, and thread-safety.

Starting at the end- Consuming the PropertyBag class

Basic usage

The best introduction to the PropertyBag is to see it being used in anger by a client. The example code uses the Address class which has a number of its own properties, and aggregates an instance of the PropertyBag.

Here is a code snippet for the consumer (Main in a console application), using the PropertyBag in its most basic form, to create a new property in the bag, and an update of the value of that property:

C#
/// Create an instance of the Address object
WebSoft.Address objAddress = new Address();


/// Set the instances in-built properties
objAddress.Line1 = "21 North Street";
objAddress.Line2 = "Northby";
objAddress.Line3 =  "";
objAddress.City = "Northtown";
objAddress.County = "Northshire";
objAddress.Country = "Northaria";
objAddress.PostCode = "NN1 1NN";

/// Set the value of the ZipCode property to 123456
/// The ZipCode property is created, as it has not
/// previously been referenced
objAddress.Properties["ZipCode"].Value = 123456;

/// Update the value of the ZipCode property from 123456
/// to 789012. The existing property is referenced, and
/// changed
objAddress.Properties["ZipCode"].Value = 789012;

/// Get the value of the ZipCode property, and write 
/// to the console
Console.WriteLine("ZipCode: {0}", 
                objAddress.Properties["ZipCode"].Value);

The example code is straightforward. The Address class has in-built properties for Line1, Line2, Line3, City, County, Country, and PostCode. It also has a Properties property which exposes an instance of the PropertyBag class.

The developer needs to add a ZipCode property to the class, and does so by manipulating the PropertyBag instance, without any re-coding.

Simply setting the Value using an index for the first time causes a new property to be added into the bag. Subsequent uses of the same index will cause the Value to be updated.

Adding "real" properties into the PropertyBag

What happens when the developer decides to add a property called Line1 to the property bag?

Having two different Line1 properties, one indexed by objAdress.Line1, and the other indexed by objAddress.Properties["Line1"].Value would introduce data duplication.

The following code snippet demonstrates how the PropertyBag class deals with this situation:

C#
/// Change the value of the Line1 property in the PropertyBag
/// (and hence the underlying value of the Address objects
/// "real" Line1 property). 
objAddress.Properties["Line1"].Value = "22 North Street";

/// Demonstrate that the "real" Line1 property has been
/// updated by writing its current value to the console
Console.WriteLine("Line1: {0}", objAddress.Line1);

When the PropertyBag is asked to index using "Line1", it uses reflection to ascertain that the Address class already has a "real" property called Line1, and sets the value on the real property.

Henceforth, use of the "Line1" index to set or get a value in the PropertyBag will be setting the underlying objAddress.Line1 property, and not a duplicate in the PropertyBag.

Serialization

The standard .NET Stream and Formatter objects can be used to serialize and deserialize the container / owner object (i.e. an instance of the Address class) including its aggregated PropertyBag and the Property object instances provided that the owner object is itself serializable.

The code to invoke serialization and deserialization is as follows:

C#
/// Create a new file called MyAddress.xml to hold the xml 
/// representation of the serialized address object
Stream objFileStreamWrite = File.Create("MyAddress.xml");

/// Create a Soap Formatter such that the serialized object 
/// will be in Simple Object Access Protocol format
SoapFormatter objSoapFormatter = new SoapFormatter();

/// It is vital to unwire the events in the Properties, 
/// as the events refer to event handlers in the 
/// test harness which cannot be deserialized. This step 
/// is critical.
objAddress.Properties["Line1"].Updated -= 
               new UpdatedEventHandler(Property_Updated);
objAddress.Properties["ZipCode"].Updating -= 
               new UpdatingEventHandler(ZipCode_Updating);
objAddress.Properties["ZipCode"].Updated -= 
               new UpdatedEventHandler(Property_Updated);

/// Serialize the address object, including the property 
/// bag into the file, and close the file
objSoapFormatter.Serialize(objFileStreamWrite, objAddress);
objFileStreamWrite.Close();

/// Open the recently (very) saved file containing the xml 
/// representation of the object
Stream objFileStreamRead = File.OpenRead("MyAddress.xml");

/// Create an instance of the deserialized address (not yet 
/// constructed)
WebSoft.Address objAddressDeserialized;

/// Instantiate the new address object, including the property 
/// bag values, from the xml file, and close the file
objAddressDeserialized = 
    (Address)(objSoapFormatter.Deserialize(objFileStreamRead));
objFileStreamRead.Close();

/// Prove that the serialization / deserialization has worked by 
/// sending a "real" property value, and
/// a "real" property value reflected by the bag, and a genuine 
/// property bag property value to the console
Console.WriteLine(objAddressDeserialized.Line1);
Console.WriteLine(objAddressDeserialized.Properties["Line1"].Value);
Console.WriteLine(objAddressDeserialized.Properties["ZipCode"].Value);

Using a property's individual Updated event

Each individual property in the bag has its own Updated event that is fired immediately after that property has had its value changed. This includes the "real" properties in the bag.

A property's Updated event can be subscribed to as follows:

C#
/// Subscribe to the Updating event of the 
/// ZipCode property within the Address objects PropertyBag

objAddress.Properties["ZipCode"].Updating += 
           new UpdatingEventHandler(ZipCode_Updating);

An event handler can then be written to deal with the event as follows:

/// <summary>
/// Event handler for the Line1 and ZipCode properties Updated event
/// </summary>
/// <param name="sender">The instance of the Property class
///               that raised the event</param>
/// <param name="e">An instance of the UpdatedEventArgs class</param>
private static void Property_Updated(object sender, UpdatedEventArgs e)
{
    /// Write a message to the console
    string strMsg = "Property {0} changed from {1} to {2}";
    Console.WriteLine(strMsg, e.Name, e.OldValue, e.NewValue);
}

Whenever the ZipCode property is updated, the Property_Updated will be fired immediately after the change. This can be useful in situations where there are multiple views on the same model. For example, the Windows Explorer contains a tree view on the left, and a list view on the right. If a folder is pasted into the list view, the tree view needs to know about it so that its own view can be updated.

One proviso is that if a particular "real" property has one, or more subscribers to the Updated event, and a client class uses the "real" property directly, the event will not be raised.

For instance, following this event subscription:

C#
/// Subscribe to the Updated event of the Line1 property in 
/// the Address objects PropertyBag 
/// (which corresponds to a "real" 
/// Line1 property within the Address object
objAddress.Properties["Line1"].Updated += 
      new UpdatedEventHandler(Property_Updated);

this code:

C#
objAddress.Properties["Line1"].Value = "22 North Street";

will raise the event, but this code:

C#
objAddress.Line1 = "23 North Street";

will not.

Using a property's individual Updating event

The Updating event has one important difference to the Updated event (apart from the obvious one that it fires before the change, and not after it!). That is, the UpdatingEventArgs argument has a Cancel property. If at least one Updating event sets the Cancel property to true, then the update will be cancelled.

This means that Updating event handlers are an ideal place to build validation for properties in the bag such that if a validation rule is broken, the update will be rejected.

Following this event subscription:

C#
/// Subscribe to the Updating event of the ZipCode 
/// property within the Address objects PropertyBag
objAddress.Properties["ZipCode"].Updating += 
       new UpdatingEventHandler(ZipCode_Updating);

An event handler may be written as follows that will ensure that the zip codes are numeric. If any client attempts to set the zip code to a non-numerical value, the update will be rejected, and the value will remain the same:

C#
/// <summary>
/// Event handler for the ZipCode properties Updating event
/// </summary>
/// <param name="sender">The instance of the Property class
///                 that raised the event</param>
/// <param name="e">An instance of the UpdatingEventArgs class</param>
private static void ZipCode_Updating(object sender, UpdatingEventArgs e)
{
    string strMsg;

    try
    {
        /// Ascertain that the proposed new value is numeric
        int i = int.Parse(e.NewValue.ToString());
        strMsg = "Property {0} about to change from {1} to {2}";
    }
    catch (System.FormatException)
    {
        /// Ascertain that the proposed new value is not numeric, 
        /// therefore set the Cancel property of the event args to true, 
        /// thereby preventing the Value of the Property from being set 
        /// to the new value
        e.Cancel = true;
        strMsg = 
          "Property {0} not changed from {1} to {2}: Zip codes are numeric";
    }
    /// Write the message to the console
    Console.WriteLine(strMsg, e.Name, e.OldValue, e.NewValue);
}

There may be multiple subscribers to the same Updating event (i.e. it is a multi-cast delegate). In this circumstance, if any single subscriber sets e.Cancel = true; then the update is cancelled, regardless of what the other subscribers do.

Finishing "starting at the end"

Hopefully, the features of the PropertyBag class are compelling enough for you to download it, and start using it in your classes. Certainly, once you understand the main features, and how to call them, you can cut straight to the chase. If you would like to peek under the hood, and understand how the PropertyBag class was constructed, read on.

Designing the PropertyBag

Before rushing off to develop the PropertyBag, it was designed using the Unified Modeling Language (UML).

An Agile approach to design was followed i.e. the design had to be just good enough to ensure that the ensuing development had been thought out, and documented. The design consisted of two sequence diagrams, and a class diagram. Being Agile, the design documentation was hand-drawn, as an electronic version of the design would have taken extra time without adding anything to the deliverable.

However, the design remit includes imparting an understanding to the readers of this article, so if any reader cannot interpret the design because of hand-writing, or general scruffiness, then please leave a blog, and I will ensure that the electronic versions are posted.

The design documents are linked, rather than embedded in the article text, because if embedded the image width would be greater than the Code Project guidelines and will introduce a horizontal scrollbar.

If the design is of interest to you, then here they are:

Please bear in mind that the OnBeforeUpdate and OnAfterUpdate events that were designed became the Updating and Updated events during development, to fall in line with the Microsoft recommended naming standards

It is also worth mentioning that when properties are set in the bag, what is actually happening is a get followed immediately by a set.

Consider the following code:

C#
/// Set the value of the ZipCode property to 123456
objAddress.Properties["ZipCode"].Value = 123456;

What is actually happening is that the Property instance indexed by objAddress.Properties["ZipCode"] is being read all the way back to the ultimate consumer, and then that Property class' Value attribute is being set.

It is only one line of code, but it is invoking both the Get and Set sequence diagram operations.

The coding at last

This section walks through the code, explaining how it all works in practice. As can be seen from a quick look at the sequence diagrams, the PropertyBag class does very little, with the majority of the work being performed inside the Property class.

The PropertyBag class

PropertyBag inherits from System.Object, and contains the following private fields:

C#
private System.Collections.Hashtable objPropertyCollection = 
                                             new Hashtable();
private System.Object objOwner;

objPropertyCollection is a System.Collections.Hashtable which contains the collection of Property objects. There was no need to construct a strongly-typed collection, as the property collection is private and is never accessed directly. All access is through the indexer.

The Owner points back to the class that contains the PropertyBag (for instance, the Address class in the examples). It is passed in the constructor and is passed on to any instantiated Property objects enabling them to reflect over the parent class in order to ascertain if a particular property (i.e. the Line1 property) is a "real" property, as opposed to an indexed member of the PropertyBag.

Apart from a constructor, and a publicly accessible property for the Owner, the only other code in the PropertyBag is the indexer:

C#
/// <summary>
/// Indexer which retrieves a property from the PropertyBag based on 
/// the property Name
/// </summary>
public Property this[string Name]
{
    get
    {
        // An instance of the Property that will be returned
        Property objProperty;        

        // If the PropertyBag already contains 
        // a property whose name matches
        // the property required, ...
        if (objPropertyCollection.Contains(Name))
        {
            // ... then return the pre-existing property
            objProperty = (Property)objPropertyCollection[Name];
        }
        else
        {
            // ... otherwise, create a new Property 
            // with a matching Name, and
            // a null Value, and add it to the PropertyBag
            objProperty = new Property(Name, Owner);
            objPropertyCollection.Add(Name, objProperty);
        }
        return objProperty;
    }
}

public Property this[string Name] is an example for the syntax of an indexer in C#. This will enable a client of the class to make a call like objAddress.Properties["ZipCode"] expecting to pass in a string, and receive an object of type Property.

I've always found the mandatory inclusion of this keyword confusing. After all, what else is the PropertyBag class going to be indexing other than the current instance of itself? My understanding of the indexers seems to increase if I blank out the presence of this keyword, maybe yours will too.

The code within the indexer is straightforward; if the Property indexed by the string Name already exists in the bag, return it, otherwise construct a new Property passing in the Name and the Owner, with a blank Value.

The blank Value may seem un-intuitive, but if we remember the footnote in the design about a set really equating to a get followed immediately by a set, we can see that the following call:

C#
/// Set the value of the ZipCode property to 123456
objAddress.Properties["ZipCode"].Value = 123456;

where the Property indexed by "ZipCode" does not already exist is created with a null Value, passed back to the client by reference, and then has its Value property set in two discrete steps, even though it is a single line of source code.

The Property class

The PropertyBag class holds a collection of Property objects in its System.Collections.Hashtable. The Property class does all the work. The Property class is an inner class of the PropertyBag class, but it is public. It has to be public, because the PropertyBag indexer returns a Property object, and the class declaration cannot be less accessible than a method which returns its type.

I will not waste any time discussing the syntax of the events. More information can be found in Using Events and Delegates in C#.

The Property constructor expects a Name, and a reference to the Owner, and sets the initial Value to null, as previously explained. All the work occurs in the Value property:

C#
public System.Object Value
{
    get
    {
        // The lock statement makes the 
        // class thread safe. Multiple threads 
        // can attempt to get the value of 
        // the Property at the same time
        lock(this)
        {
            // Use reflection to see if the client 
            // class has a "real" property 
            // that is named the same as the 
            // property that we are attempting to
            // retrieve from the PropertyBag
            System.Reflection.PropertyInfo p = 
                Owner.GetType().GetProperty(Name);

            if (p != null)
            {
                // If the client class does have a 
                // real property thus named, return 
                // the current value of that "real" 
                // property
                return p.GetValue(Owner, new object[]{});
            }
            else
            {
                // If the client class does not 
                // have a real property thus named, 
                // return this Property objects 
                // Value attribute
                return this.objValue;
            }
        }
    }
    ...
}

The lock statement, together with its complementary statement in the set ensure whilst one thread is getting the value, no other thread can set it and vice versa (as a side-effect, it prevents multiple threads from simultaneously getting the Value).

get uses the System.Reflection.PropertyInfo class, making use of the Owner object to ascertain if the owner has a "real" property named (e.g. Line1). If it does, then the value of the "real" property is read using PropertyInfo.GetValue, and returned to the client. If it does not, the Property instance's own Value is returned.

C#
public System.Object Value
{
    ...
    set
    {
        // The lock statement makes the class 
        // thread safe. Multiple threads 
        // can attempt to set the value of 
        // the Property at the same time
        lock(this)
        {
            // Reflection is used to see if the client class
            // has a "real" property 
            // that is named the same as the 
            // property that we are attempting to
            // set in the PropertyBag
            System.Reflection.PropertyInfo objPropertyInfo = 
                              Owner.GetType().GetProperty(Name);
        
            // Placeholder for the old value
            System.Object objOldValue = null;


            // If objPropertyInfo is not null, ...
            if (objPropertyInfo != null)
            {
                // ... then the client class has 
                // a real property thus named, 
                // save the current value of that 
                // real property into objOldValue
                objOldValue = 
                  objPropertyInfo.GetValue(Owner, new object[]{});
            }
            else
            {
                // ... otherwise the client class does
                // not have a real property thus 
                // named, save the current value
                // of this Property objects Value attribute
                objOldValue = this.objValue;
            }

            // Create a sub-class of EventArgs to 
            // hold the event arguments
            WebSoft.UpdatingEventArgs objUpdatingEventArgs = 
                new UpdatingEventArgs(Name, objOldValue, value);

            // Execute a synchronous call to each subscriber
            OnUpdating(objUpdatingEventArgs);

            // If one or more subscribers set the Cancel property
            // to true, this means that
            // the update is cancelled in an Updating event
            // (maybe validation has 
            // failed), so the the function returns immediately
            if (objUpdatingEventArgs.Cancel)
            {
                return;
            }

            // If the client class has a "real" property
            // matching this Property Name, ...
            if (objPropertyInfo != null)
            {
                // ... then set that "real" property to 
                // the new value
                objPropertyInfo.SetValue(Owner, value, new object[]{});
            }
            else
            {
                // ... otherwise, set the Value attribute
                // of the current property object
                this.objValue = value;
            }

            // ... Execute a synchronous call to each subscriber
            OnUpdated(new UpdatedEventArgs(Name, objOldValue, value));
        }
    }
}

The set works along similar lines, but has a little extra complexity to enable the events to be implemented.

Again, lock is used to make the class safe for multi-threaded access. System.Reflection.PropertyInfo is used as it was in the get to read the "real" value, or the current Property.Value accordingly. This is needed in case there are any subscribers to the Updating or Updated events, both of which require the old, i.e. the pre-update value.

A WebSoft.UpdatingEventArgs object is instantiated, and passed to the protected method OnUpdating. If one or more subscribers set the WebSoft.UpdatingEventArgs object's Cancel property to true, then the update is cancelled by an immediate return from the property set.

Next, System.Reflection.PropertyInfo is used again to call SetValue on the "real" property, if it exists, otherwise the Value of the current Property instance is set.

Finally, a call is made to OnUpdated to signal any subscribers to the Updated event.

The remainder of the code implements the protected methods to raise the events, simple property gets, and a class for UpdatedEventArgs, and one for UpdatingEventArgs which inherits from UpdatedEventArgs, extending it by virtue of the inclusion of a Cancel property to enable subscribers to the Updating event to reject the proposed new value if they do not like it.

Finally, the delegates for UpdatingEventHandler and UpdatedEventHandler are declared.

Conclusion

The PropertyBag class is a useful utility for any class to aggregate that may need to have its standard range of properties expanded. The design demonstrates the agile approach to designing using UML, and provides a good example of a UML class diagram, and UML sequence diagrams. Studying the implementation of the class provides concrete examples of indexers, inner classes, reflection, events, and thread safety.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
South Africa South Africa
Graham works as a senior developer / software architect for WebSoft Consultants Limited, devloping using a number of software technologies, concentrating on the C# / .NET technology set.

Areas of expertise include document management, n-tier enterprise business systems, and exploitation of workflow technology, as well as a wide range of development methodologies including agile model driven development, and UML.

Comments and Discussions

 
QuestionMissing images? Pin
_Noctis_30-Jun-14 14:49
professional_Noctis_30-Jun-14 14:49 
QuestionVery Well Explained Pin
Vambak14-Oct-13 0:16
Vambak14-Oct-13 0:16 
QuestionXmlserialization for PropertyBag Pin
naga harish10-Nov-09 20:03
naga harish10-Nov-09 20:03 
Questionaccess control? Pin
Waterine3-Jan-09 21:37
Waterine3-Jan-09 21:37 
Generalunable to run this project Pin
SarathTata12-Sep-08 6:53
SarathTata12-Sep-08 6:53 
Generalcannot download the UML diagrams Pin
Mike20026-May-08 5:19
Mike20026-May-08 5:19 
GeneralSupport MS databinding Pin
maltmann3-Feb-06 7:16
maltmann3-Feb-06 7:16 
In order to support two-way databinding with the Microsoft databinding architecture, you need to emit the proper events when a property changes.
I'd suggest the following change:
public override void SetValue(object component, object value)
{
// Have the property bag raise an event to set the current value
// of the property.
PropertySpecEventArgs e = new PropertySpecEventArgs(item, value);
bag.OnSetValue(e);
// Add this next line to MS databinding knows about the change
base.OnValueChanged(bag,e);
}

Michael
GeneralNice, but.... Pin
mBonafe2-Feb-06 9:13
mBonafe2-Feb-06 9:13 
GeneralRe: Nice, but.... Pin
Graham Harrison15-Feb-06 21:04
Graham Harrison15-Feb-06 21:04 
GeneralPropertyGrid Pin
[Gone]14-Jan-06 6:02
[Gone]14-Jan-06 6:02 
GeneralRe: PropertyGrid Pin
FreeRider130-Jan-06 3:57
FreeRider130-Jan-06 3:57 
GeneralSuggestion to speedup the class Pin
AndyHo20-Dec-05 3:53
professionalAndyHo20-Dec-05 3:53 
GeneralRe: Suggestion to speedup the class Pin
Graham Harrison29-Dec-05 21:38
Graham Harrison29-Dec-05 21:38 
QuestionPersistance? Pin
Doncp27-Nov-05 18:51
Doncp27-Nov-05 18:51 
AnswerRe: Persistance? Pin
Graham Harrison28-Nov-05 11:28
Graham Harrison28-Nov-05 11:28 
GeneralRe: Persistance? Pin
Doncp28-Nov-05 13:59
Doncp28-Nov-05 13:59 
GeneralRe: Persistance? Pin
Graham Harrison28-Nov-05 21:41
Graham Harrison28-Nov-05 21:41 
AnswerRe: Persistance? Pin
Adam L. Stevenson29-Nov-05 17:08
Adam L. Stevenson29-Nov-05 17:08 
GeneralRe: Persistance? Pin
Graham Harrison29-Nov-05 21:07
Graham Harrison29-Nov-05 21:07 
GeneralRe: Persistance? Pin
Adam L. Stevenson29-Nov-05 21:21
Adam L. Stevenson29-Nov-05 21:21 
GeneralRe: Persistance? Pin
Graham Harrison30-Nov-05 1:28
Graham Harrison30-Nov-05 1:28 
GeneralRe: Persistance? Pin
Stan4th2-Dec-05 6:13
Stan4th2-Dec-05 6:13 

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

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