|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionThis article details the design, coding, and documentation for a When classes are designed, they are given the properties that the designer envisages the clients of that class require. For example, an It is common for classes to be used outside of the scope intended by the designer. For instance, our hypothetical On one hand, the Requirements of the PropertyBag classGetting and setting propertiesThe main requirements of the Ease of useA 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 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 Deal with the existing properties intuitivelyAnother feature is to take account of the "real" properties of the class that contains the For instance, if the parent class is the Use of reflection within the Implement updating and updated events on individual propertiesThe properties within the 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 SerializabilityThe Thread safetyThe final requirement is for the The writing of properties, and the reading of properties that can be written within the ConclusionThe Starting at the end- Consuming the PropertyBag classBasic usageThe best introduction to the Here is a code snippet for the consumer ( /// 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 The developer needs to add a Simply setting the Adding "real" properties into the PropertyBagWhat happens when the developer decides to add a property called Having two different The following code snippet demonstrates how the /// 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 Henceforth, use of the " SerializationThe standard .NET The code to invoke serialization and deserialization is as follows: /// 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 eventEach individual property in the bag has its own A property's /// 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 One proviso is that if a particular "real" property has one, or more subscribers to the For instance, following this event subscription: /// 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: 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 eventThe This means that Following this event subscription: /// 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: /// <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 Finishing "starting at the end"Hopefully, the features of the Designing the PropertyBagBefore rushing off to develop the 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 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: /// Set the value of the ZipCode property to 123456
objAddress.Properties["ZipCode"].Value = 123456;
What is actually happening is that the It is only one line of code, but it is invoking both the Get and Set sequence diagram operations. The coding at lastThis 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 The PropertyBag class
private System.Collections.Hashtable objPropertyCollection =
new Hashtable();
private System.Object objOwner;
The Apart from a constructor, and a publicly accessible property for the /// <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;
}
}
I've always found the mandatory inclusion of The code within the indexer is straightforward; if the The blank /// Set the value of the ZipCode property to 123456
objAddress.Properties["ZipCode"].Value = 123456;
where the The Property classThe 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 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
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 Again, A Next, Finally, a call is made to The remainder of the code implements the Finally, the delegates for ConclusionThe
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||