Click here to Skip to main content
15,886,199 members
Articles / Programming Languages / C#

Unlimited baselines for any class

Rate me:
Please Sign up or sign in to vote.
4.82/5 (6 votes)
11 Jun 2009CPOL9 min read 23.4K   134   31  
Using the ComponentModel namespace to implement unlimited baselines.

image011.png

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 DataColumns. But, what fun would that be?

Let's make a Person class, one of the properties of which is of Address type.

C#
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:

image001.png

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.

C#
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.

C#
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.

C#
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.

C#
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:

C#
[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:

XML
<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:

image003.png

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.

C#
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?

image005.png

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.

C#
[AttributeUsage(AttributeTargets.Property)]
class BaselinePropertyAttribute : Attribute
{
}

These are the changes to the Person class:

C#
[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.

C#
class Person :  CustomTypeDescriptor
{
...
...
    public override PropertyDescriptorCollection 
                    GetProperties(Attribute[] attributes)
    {
        // TODO: Replicate baseline properties.
        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.

C#
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()));

    // Array of replicated property descriptors, which describe baseline properties.
    List<PropertyDescriptor> replicatedBaselinePds = new List<PropertyDescriptor>();

    // Assume there is a baseline. Assume it is "baseline 1".
    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.

C#
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.

image007.png

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:

C#
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:

C#
public override object GetValue(object component)
{
    Person person = (Person)component;

    // Get a specific baseline value of class Person by calling the
    // GetBaselineProperty method.
    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;

    // Set a specific baseline value of class Person by calling the
    // SetBaselineProperty method.
    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.

image009.png

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.

C#
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.

image011.png

Everything works properly and as expected to the point of boredom.

License

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


Written By
Software Developer (Senior)
Canada Canada
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --