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:
WebSoft.Address objAddress = new Address();
objAddress.Line1 = "21 North Street";
objAddress.Line2 = "Northby";
objAddress.Line3 = "";
objAddress.City = "Northtown";
objAddress.County = "Northshire";
objAddress.Country = "Northaria";
objAddress.PostCode = "NN1 1NN";
objAddress.Properties["ZipCode"].Value = 123456;
objAddress.Properties["ZipCode"].Value = 789012;
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:
objAddress.Properties["Line1"].Value = "22 North Street";
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:
Stream objFileStreamWrite = File.Create("MyAddress.xml");
SoapFormatter objSoapFormatter = new SoapFormatter();
objAddress.Properties["Line1"].Updated -=
new UpdatedEventHandler(Property_Updated);
objAddress.Properties["ZipCode"].Updating -=
new UpdatingEventHandler(ZipCode_Updating);
objAddress.Properties["ZipCode"].Updated -=
new UpdatedEventHandler(Property_Updated);
objSoapFormatter.Serialize(objFileStreamWrite, objAddress);
objFileStreamWrite.Close();
Stream objFileStreamRead = File.OpenRead("MyAddress.xml");
WebSoft.Address objAddressDeserialized;
objAddressDeserialized =
(Address)(objSoapFormatter.Deserialize(objFileStreamRead));
objFileStreamRead.Close();
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:
objAddress.Properties["ZipCode"].Updating +=
new UpdatingEventHandler(ZipCode_Updating);
An event handler can then be written to deal with the event as follows:
private static void Property_Updated(object sender, UpdatedEventArgs e)
{
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:
objAddress.Properties["Line1"].Updated +=
new UpdatedEventHandler(Property_Updated);
this code:
objAddress.Properties["Line1"].Value = "22 North Street";
will raise the event, but this code:
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:
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:
private static void ZipCode_Updating(object sender, UpdatingEventArgs e)
{
string strMsg;
try
{
int i = int.Parse(e.NewValue.ToString());
strMsg = "Property {0} about to change from {1} to {2}";
}
catch (System.FormatException)
{
e.Cancel = true;
strMsg =
"Property {0} not changed from {1} to {2}: Zip codes are numeric";
}
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:
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:
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:
public Property this[string Name]
{
get
{
Property objProperty;
if (objPropertyCollection.Contains(Name))
{
objProperty = (Property)objPropertyCollection[Name];
}
else
{
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:
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:
public System.Object Value
{
get
{
lock(this)
{
System.Reflection.PropertyInfo p =
Owner.GetType().GetProperty(Name);
if (p != null)
{
return p.GetValue(Owner, new object[]{});
}
else
{
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.
public System.Object Value
{
...
set
{
lock(this)
{
System.Reflection.PropertyInfo objPropertyInfo =
Owner.GetType().GetProperty(Name);
System.Object objOldValue = null;
if (objPropertyInfo != null)
{
objOldValue =
objPropertyInfo.GetValue(Owner, new object[]{});
}
else
{
objOldValue = this.objValue;
}
WebSoft.UpdatingEventArgs objUpdatingEventArgs =
new UpdatingEventArgs(Name, objOldValue, value);
OnUpdating(objUpdatingEventArgs);
if (objUpdatingEventArgs.Cancel)
{
return;
}
if (objPropertyInfo != null)
{
objPropertyInfo.SetValue(Owner, value, new object[]{});
}
else
{
this.objValue = value;
}
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.