Introduction
In very general terms, a baseline is a preserved state of a system. Progress is measured by comparing the current state to one of the previously
persisted states. From a programming point of view, saving a baseline means taking a snapshot of the current properties of an object.
The more baselines that get created - the longer is the history of each property of an object.
A simple class
There is a substantial support in the industry for browsing and modifying properties of objects. One example is the Property Grid in Visual Studio.
There are other grids that display objects' properties given property names. Storing baseline information in the form of properties is the way to go in my opinion.
Let's start with a very simple class, and gradually improve it to support unlimited baselines.
PropertyGrid basics
Since I plan to deal extensively with properties and property descriptors, I better get a basic understanding of the debugging tool
of choice: the PropertyGrid
. If PropertyGrid
does my bidding, that means I am going in the right direction.
Otherwise, I might just create a DataTable
that contains baseline properties as DataColumn
s. But, what fun would that be?
Let's make a Person
class, one of the properties of which is of Address
type.
class Address
{
public string City
{
get;
set;
}
public string Street
{
get;
set;
}
}
class Person
{
public Guid Guid
{
get;
set;
}
public string Name
{
get;
set;
}
public int Age
{
get;
set;
}
public Address Address
{
get;
set;
}
}
What if we create a form with a PropertyGrid
, and assign a Person
instance to the grid? We will have the following results:
All properties are at their default values. The Address
property is grayed out because it is not editable.
To enable editing properties of custom classes, those classes must have an associated TypeConverter class that implements conversion from a string.
That is, CanConvertFrom
and ConvertFrom
a string are implemented. MSDN recommends inheriting from ExpandableObjectConverter
to make this
task simpler. (See chapter "Adding Expandable Property Support in MSDN).
Let's do just that. Create an AddressConverter
class that inherits from ExpandableObjectConverter
;
override the CanConvertTo
, ConvertTo
, CanConvertFrom
, and ConvertFrom
methods.
CanConvertTo
tells whether a given type converter can convert to the destination type. AddressConverter
can convert
to Address
type, and therefore returns true when the destination type is an Address
.
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType.IsAssignableFrom(typeof(Address)))
{
return true;
}
return base.CanConvertTo(context, destinationType);
}
CanConvertFrom
tells whether a given type converter can convert from the source type. AddressConverter
can convert
from the string type, and returns true
in this case.
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string))
{
return true;
}
return base.CanConvertFrom(context, sourceType);
}
To avoid coming up with a format of the string representation of our class, and especially avoid parsing that string,
I selected to let the XmlSerializer
class do all the hard work for me.
ConvertTo
serializes an Address
to a string.
public override object ConvertTo(ITypeDescriptorContext context,
System.Globalization.CultureInfo culture, object value, Type destinationType)
{
if (destinationType == typeof(string) && value is Address)
{
return SerializeAddressAsString(value as Address);
}
return base.ConvertTo(context, culture, value, destinationType);
}
private string SerializeAddressAsString(Address address)
{
if (address == null)
{
return string.Empty;
}
StringBuilder sb = new StringBuilder();
using (XmlWriter xwriter = XmlWriter.Create(sb))
{
XmlSerializer addressSerializer = new XmlSerializer(typeof(Address));
addressSerializer.Serialize(xwriter, address);
return sb.ToString();
}
}
ConvertFrom
deserializes an Address
instance from a string.
public override object ConvertFrom(ITypeDescriptorContext context,
System.Globalization.CultureInfo culture, object value)
{
if (value is string)
{
return DeserializeAddressFromString(value as string);
}
return base.ConvertFrom(context, culture, value);
}
private Address DeserializeAddressFromString(string serializedAddress)
{
if (string.IsNullOrEmpty(serializedAddress))
{
return null;
}
XmlSerializer addressSerializer = new XmlSerializer(typeof(Address));
return (Address)addressSerializer.Deserialize(new StringReader(serializedAddress));
}
Apply a TypeConverterAttribute
to the Address
type to let it know what type converter to use:
[TypeConverterAttribute(typeof(AddressConverter))]
public class Address
Running the program now shows an editable Address
box. We should enter an XML string in it; for instance, this:
<address><city>Whitehorse</city><street>1 Alexander St</street></address>
It is a valid XML that can be deserialized into an Address
object. PropertyGrid
in the application now displays
both properties of the Address
properly set:
While we are at it, let's take it one step further. While the XML string works fine and was a breeze to implement, it requires a bit
of typing. Even more, it requires a knowledge of class name and class properties in order to construct the XML in the first place. That's not
very user friendly, even annoying. Let's display a small form with two entry fields instead. That is, make a custom property editor,
like all self-respecting component publishers.
Custom UI for the properties of Address type
PropertyGrid
will show a custom user interface for properties that have an associated UITypeEditor
. There are several
simple steps to take in order to create a fully functional custom UI type editor.
First, we need a UI class to display Address
properties. The UI class can be either shown as a modal form or as a drop-down. I chose
to implement it as a modal form. My AddressEditorForm
class contains two textboxes and two public properties to access contents of these textboxes.
Next, we need to tell the PropertyGrid
how to use AddressEditorForm
in order to edit an Address
.
This is the responsibility of AddressUITypeEditor
, which inherits from UITypeEditor
. AddressUITypeEditor
instructs
the PropertyGrid
how the UI component is to be shown. It acts as a bridge between the UI that is shown to the user and the property in the grid.
Override the GetEditStyle
method in order to instruct PropertyGrid
how AddressEditorForm
is to be shown.
AddressEditorForm
is shown as UITypeEditorEditStyle.Modal
.
Override the EditValue
method to display the AddressEditorForm
, and use values that it returns to set new Address
values or update existing ones.
class AddressUITypeEditor : UITypeEditor
{
public override UITypeEditorEditStyle
GetEditStyle(System.ComponentModel.ITypeDescriptorContext context)
{
return UITypeEditorEditStyle.Modal;
}
public override object EditValue(System.ComponentModel.ITypeDescriptorContext
context, IServiceProvider provider, object value)
{
IWindowsFormsEditorService service =
(IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService));
Address address = value as Address;
AddressEditorForm addressEditor = new AddressEditorForm();
if (address != null)
{
addressEditor.Street = address.Street;
addressEditor.City = address.City;
}
if (DialogResult.OK == service.ShowDialog(addressEditor))
{
return new Address
{
City = addressEditor.City,
Street = addressEditor.Street
};
}
return address;
}
}
What will be the result of this code when the program is run?
Clicking inside the Address
property shows the ellipsis, which in turn pops up AddressEditorForm
in the modal form.
Address
is still serialized to and from an XML string, but now there is no need to remember the class structure and enter it manually.
At this point, the Person
class has been transformed into a class that is easily editable using a PropertyGrid
.
We can move on to the main subject of this article: unlimited baselines.
Selecting properties to track
An instance of Person
, Bob
, is born. Which properties of Bob
will likely change? Guid
will remain constant throughout
Bob
's lifetime. The Name "Bob" will most likely remain unchanged as well. We are interested in tracking two properties: Bob's age and address.
These properties of interest will be marked by a BaselineProperty
attribute. BaselinePropertyAttribute
can only be applied to properties of a class.
[AttributeUsage(AttributeTargets.Property)]
class BaselinePropertyAttribute : Attribute
{
}
These are the changes to the Person
class:
[BaselineProperty]
public int Age
{
get;
set;
}
[BaselineProperty]
public Address Address
{
get;
set;
}
Dynamically adding and removing object properties
Baselines will be implemented as dynamic bindable object properties. The Person
class needs to be able to add and remove these properties at will,
meaning the class needs to provide type information about itself. That is, the class either has to implement the ICustomTypeDescriptor
interface
or inherit from the CustomTypeDescriptor
class.
Inheriting from the CustomTypeDescriptor
is simpler in the case of this example. We are only interested in tinkering with the functionality that
describes the properties of the class. Overall, there are three functions to override to get back the default behavior we had before.
class Person : CustomTypeDescriptor
{
...
...
public override PropertyDescriptorCollection
GetProperties(Attribute[] attributes)
{
return TypeDescriptor.GetProperties(this, attributes, true);
}
public override PropertyDescriptorCollection GetProperties()
{
return GetProperties(null);
}
public override object GetPropertyOwner(PropertyDescriptor pd)
{
return this;
}
...
...
}
Partially, baseline magic will occur in the GetProperties()
method of the class Person
.
We do not have a user interface to add or remove a baseline yet. This is not required for a simple proof of concept.
Let's assume there is a single baseline added. Say, baseline 1. Properties that were marked by BaselinePropertyAttribute
are Address
and Age
. If baseline 1 exists, I expect to see the following properties of object
Bob
: Address
, Age
, Guid
, Name
, Address1
, and Age1
.
The GetProperties()
method is modified to create a property descriptor collection that contains replicated baseline properties. To replicate
baseline properties, we will copy the property descriptors of those properties that are marked by BaselinePropertyAttribute
,
and then combine the baseline property descriptors with the default ones into a single collection.
public override PropertyDescriptorCollection GetProperties(Attribute[] attributes)
{
List<PropertyDescriptor> defaultPds =
TypeDescriptor.GetProperties(this, attributes, true).Cast<PropertyDescriptor>().ToList();
IEnumerable<PropertyDescriptor> baselinePds =
defaultPds.Where(x => x.Attributes.Contains(new BaselinePropertyAttribute()));
List<PropertyDescriptor> replicatedBaselinePds = new List<PropertyDescriptor>();
List<int> baselines = new List<int>(new int[] { 1 });
foreach (int baseline in baselines)
{
foreach (PropertyDescriptor pd in baselinePds)
{
replicatedBaselinePds.Add(new BaselinePropertyDescriptor(pd, baseline));
}
}
return new PropertyDescriptorCollection(defaultPds.Union(replicatedBaselinePds).ToArray());
}
Since the baseline property behaves the same way as the original, the baseline property should have an almost identical PropertyDescriptor
.
PropertyDescriptor
is not a cloneable class. The best way to make a copy of one is to create a child class and use one of the initializer
overloads to pass an instance of another PropertyDescriptor
. Unfortunately, this does not allow changing the
Name
property of the PropertyDescriptor
, which is absolutely required in the case of this article.
For instance, the descriptor of the property Age
must be copied and given a new name Age1
.
There are no initializers that make a complete copy of a PropertyDescriptor
and allow Name
change at the same time.
The next best thing is to set the Name
to a required value and use as much an existing PropertyDescriptor
as possible.
The property descriptor of the original property will be encapsulated by the child class and used to implement the child's abstract members.
class BaselinePropertyDescriptor : PropertyDescriptor
{
PropertyDescriptor _baseDescriptor = null;
public BaselinePropertyDescriptor(PropertyDescriptor descriptor, int baseline)
: base(GetPdName(descriptor, baseline), GetPdAttribs(descriptor))
{
_baseDescriptor = descriptor;
}
private static string GetPdName(PropertyDescriptor descriptor, int baseline)
{
return string.Format("{0}{1}", descriptor.Name, baseline);
}
private static Attribute[] GetPdAttribs(PropertyDescriptor descriptor)
{
return descriptor.Attributes.Cast<Attribute>().ToArray();
}
public override bool CanResetValue(object component)
{
return _baseDescriptor.CanResetValue(component);
}
public override Type ComponentType
{
get { return _baseDescriptor.ComponentType; }
}
public override object GetValue(object component)
{
return _baseDescriptor.GetValue(component);
}
public override void ResetValue(object component)
{
_baseDescriptor.ResetValue(component);
}
public override void SetValue(object component, object value)
{
_baseDescriptor.SetValue(component, value);
}
...
}
Let's run the program and see the effects of implementing GetProperties()
and BaselinePropertyDescriptor
the way it is done in this example.
There are the baseline properties Address1
and Age1
alright! However, changing the value of Age
modifies the value of Age1
and vice versa.
The same problem occurs with Address
fields. What gives?
The answer is obvious: it's the way BaselinePropertyDescriptor
is implemented. We have changed the name of the property,
but we use the original PropertyDescriptor
's GetValue()
and SetValue()
methods. Those methods will, of course, modify the original
Bob.Age
property. These functions will not look for a property Bob.Age1
simply because they are called from a PropertyDescriptor
,
whose Name
is Age1
. The GetValue
and SetValue
methods have to be modified to access the Age1
value,
wherever that value may be. Two tasks to accomplish are: first, values of baseline properties must be stored somewhere.
Second, the GetValue()
, SetValue()
, ResetValue()
, and ShouldSerialize()
methods must know how to access these properties in memory.
Storing values of baseline properties
Data structures chosen to store baseline properties are a matter of design. I am going ahead with the first idea that came to my mind
as I did not find a simpler, easier, and cleaner solution. Maybe real-world classes would invite a completely different solution,
but my Person
class does not suggest anything in particular.
Baseline properties' values will be stored in a private dictionary in class Person
. Keys will be property names,
such as Age1
, Address1
, etc. Values will be baseline property values stored as objects.
The data structure will be a Dicrionary<string>
. Its contents will look similar to the following:
Age1 --> 1 |
Address1 --> 123 SomeStreet, SomeCity |
Age2 --> 2 |
Address2 --> 123 SomeStreet, SomeCity |
Accessing baseline data from PropertyDescriptor
Now that baseline data is stored in a dictionary, we need a way to access it from BaselinePropertyDescriptor
.
Again, the way to access this data will vary widely based on a particular design. It looks reasonable to create two private methods
in the class Person
: GetBaselineProperty()
and SetBaselineProperty()
, to manipulate baseline data. These private methods
will be called using Reflection from the BaselinePropertyDescriptor
.
For example, to set the baseline property Age1
, the following will be called from the PropertyDescriptor
: SetBaselineProperty("Age1", 1)
.
To get a baseline value Age1
, the following will be called from the PropertyDescriptor
: GetBaselineProperty("Age1")
.
Changes made to the Person
class:
private Dictionary<string> _BaselineData = new Dictionary<string>();
private void SetBaselineProperty(string propertyName, object value)
{
if (!_BaselineData.ContainsKey(propertyName))
{
throw new MissingFieldException(this.GetType().Name, propertyName);
}
_BaselineData[propertyName] = value;
}
private object GetBaselineProperty(string propertyName)
{
if (!_BaselineData.ContainsKey(propertyName))
{
throw new MissingFieldException(this.GetType().Name, propertyName);
}
return _BaselineData[propertyName];
}
Changes made to the BaselinePropertyDescriptor
class:
public override object GetValue(object component)
{
Person person = (Person)component;
Type t = typeof(Person);
MethodInfo getBaselineProperty = t.GetMethod("GetBaselineProperty",
BindingFlags.NonPublic | BindingFlags.Instance);
return getBaselineProperty.Invoke(person, new object[] { Name });
}
public override void SetValue(object component, object value)
{
Person person = (Person)component;
Type t = typeof(Person);
MethodInfo setBaselineProperty = t.GetMethod("SetBaselineProperty",
BindingFlags.NonPublic | BindingFlags.Instance);
setBaselineProperty.Invoke(person, new object[] { Name, value });
}
public override void ResetValue(object component)
{
Person person = (Person)component;
object value = Activator.CreateInstance(_baseDescriptor.PropertyType);
SetValue(component, value);
}
public override bool ShouldSerializeValue(object component)
{
Person person = (Person)component;
object defaultValue = Activator.CreateInstance(_baseDescriptor.PropertyType);
object currentValue = GetValue(component);
return !object.Equals(currentValue, defaultValue);
}
At this point, baseline data is stored in proper locations in memory in the class Person
. This data is accessed using
Reflection from the class BaselinePropertyDescriptor
. It is time to run the program and see what results these changes produce.
Perfect! The proof of concept is a success.
Finishing touches
The core functionality is done. A proof of concept test case created a valid baseline. Now, it is time to finish off the rest of the basic
baseline features. And, by basic, I mean adding a baseline and removing a baseline.
The Person
class gets a list of integers that stores the existing baselines. It also gets functions to add a baseline,
to remove a baseline, and to verify whether a baseline exists.
internal bool BaselineExists(int baseline)
{
return _Baselines.Any(x => x == baseline);
}
internal void AddBaseline(int baseline)
{
if (BaselineExists(baseline))
{
throw new InvalidOperationException(
string.Format("Baseline {0} exists", baseline));
}
_Baselines.Add(baseline);
IEnumerable<PropertyDescriptor> defaultPds =
TypeDescriptor.GetProperties(this, true).Cast<PropertyDescriptor>();
IEnumerable<PropertyDescriptor> baselinePds =
defaultPds.Where(x => x.Attributes.Contains(new BaselinePropertyAttribute()));
foreach (PropertyDescriptor pd in baselinePds)
{
string strPropertyName = string.Format("{0}{1}", pd.Name, baseline);
_BaselineData[strPropertyName] = null;
}
}
The user interface uses these methods to add and remove baselines.
Here is the end result of adding baselines 1, 5, and 6.
Everything works properly and as expected to the point of boredom.