Click here to Skip to main content
11,502,348 members (63,438 online)
Click here to Skip to main content

Using IDesigner and CodeDomSerializer to create read-only runtime properties

, 3 Feb 2005 42.3K 1.3K 45
Rate this:
Please Sign up or sign in to vote.
An approach to implementing a Component that has read-only run time properties that are read/write at design time.

Introduction

When I was creating the MRUHandler component, I wanted to implement properties that could be set at design time, but would be read-only at runtime. I didn't want to have to provide a "setter" method, but still allow the developer to set these properties at design time. The MRUHandler implementation did provide this functionality, but I figured there had to be a more efficient way.

This article describes the evolution that I went through to create a reusable component base to implement serialize properties that don't have a "setter" method defined, but can have the properties set in the designer.

1st Approach

The MRUHandler component was to implement the ISupportInitialize interface. This interface provided two methods: BeginInit() and EndInit(). These methods allowed a flag to be set to indicate when initialization had started and completed.

Within each read-only runtime property, a check was made to only allow the value of the property to be set in design mode or during initialization.

public class MRUHandler : Component, ISupportInitialize {
    private bool _isInitializing = false;
    public void BeginInit() {_isInitializing = true;}
    public void EndInit() {_isInitializing = false;}
    public MenuItem MruItem {
        get{ return _mruItem;  }
        set{
            if (!DesignMode && !_isInitializing) {
               throw new Exception("The 'MruItem' can" + 
                         " be only set in Design Mode");
            }
            _mruItem = value;
        }
    }
}

This approach worked to a point. If the application attempted to set the property outside of the context of the initializing, it would throw an exception. The problem I had with this was, the exception was thrown at runtime as opposed to design time. If the property is truly read-only, then the fact that the developer tried to assign a value to a read-only property should be caught at design time, not at runtime.

2nd Approach

The 2nd approach was documented in this original article. This approach extended ComponentDesigner to provide shadow properties that allowed property values to be set at design time. Each shadow property had the DesignOnly attribute set, so that it was not serialized to the generated code. It would be, instead, serialized to the resource file of the containing form. In addition, CodeDomSerializer was extended so that the generated constructor of our component was modified to provide several parameters that allowed the component to find its read only property values in the resource file.

Again, this worked to a point, but had some challenges that needed to be overcome:

  • How do I serialize objects that don't convert nicely into strings? Intrinsic elements worked nicely, but once you tried anything like a MenuItem, it just didn't work. The standard serializer just ignored those values.
  • How do I avoid rewriting this functionality every time I have a new component that needs initialized read-only properties?
  • How do I tell when the component is used that a particular property is interested in being aware of component name changes?
  • How do I determine which properties need to have a design time implementation?
  • How do I set the property at runtime if it is not serialized in the resource file and I don't have a setter function?

3rd Approach

To address these challenges, I decided to develop a new component, RuntimePropertiesBase, which would be the base class that I could derive other components from. The only purpose of this class was to provide a common point from which the Designer and DesignerSerializer attributes could be defined and provide a common public method to initialize the read only properties: InitializeReadonlyProperty.

A new code serializer RuntimePropertiesSerializer was developed to generate calls to the InitializeReadonlyProperty method. ComponentDesigner was again extended to provide the handling of shadow properties. Several custom attributes ComponentNameListener and UpdateReadOnly where defined to facilitate read-only property behavior.

RuntimeProperties Component Design

ComponentNameListenerAttribute

The attribute ComponentNameListenerAttribute is defined for each property on the derived component that need to be changed if the user changes the name of the component at design time.

using System;
[AttributeUsage(AttributeTargets.Property)]
public class ComponentNameListenerAttribute : Attribute {
    public ComponentNameListenerAttribute() {}
}

UpdateReadOnlyAttribute

The attribute, UpdateReadOnlyAttribute, identifies properties of the derived component that need to have design time shadow properties created. The parameter provides the runtime with the internal variable that is used by the property to store its value. As we will see in the next section, the InitializeReadonlyProperty method uses this attribute to find the internal field to set the value using reflection.

[AttributeUsage(AttributeTargets.Property)]
public class UpdateReadOnlyAttribute : Attribute {
    public UpdateReadOnlyAttribute(string fieldName) {
        _fieldName = fieldName;
    }

    public string FieldName {get {return _fieldName;}}
}

RuntimePropertiesBase

The InitializeReadonlyProperty is not very interesting when running in design mode as we have access to the shadow properties and can just set the value. Where it gets more interesting is when it is called during run time.

At this point, we need to find the property specified by the propertyName parameter. Using reflection, we find the corresponding PropertyInfo. From the PropertyInfo, we can find the UpdateReadOnly attribute. This attribute provides the internal field name. Armed with the field name, we can get the FieldInfo for this field. We can then call the SetValue method to initialize the value.

[DesignerAttribute(typeof(RuntimePropertiesDesigner)),
 DesignerSerializer(typeof(RuntimePropertiesSerializer),
                    typeof(CodeDomSerializer))]
    public abstract class RuntimePropertiesBase : Component, 
                                        ISupportInitialize {
        private bool _isInitializing = false;

        public void InitializeReadonlyProperty(string propertyName,
                                               object propertyValue) {
            //    check for design or run mode
            if (DesignMode) {
                PropertyDescriptor pd = 
                     TypeDescriptor.GetProperties(this)[propertyName];
                pd.SetValue(this, propertyValue);
            }
            else {
                if (!_isInitializing) {
                    throw new Exception("The '"+propertyName+
                              "' property can only be set in Design Mode");
                }
                //    get the type definition
                Type t = this.GetType();
 
                //    set the binding flags to search
                BindingFlags flags = BindingFlags.NonPublic |
                                     BindingFlags.Instance |
                                     BindingFlags.Public;
                PropertyInfo pi = t.GetProperty(propertyName, flags);
                
                object [] attrs = pi.GetCustomAttributes(
                                 typeof(UpdateReadOnlyAttribute),
                                 false);
                //    If there is a UpdateReadOnlyAttribute
                if (attrs != null && attrs.Length > 0) {
                    UpdateReadOnlyAttribute uroa = 
                           (UpdateReadOnlyAttribute)attrs[0]);
                    string fieldName = uroa.FieldName;
                    
                    //    get the fields of the component
                    FieldInfo [] fields = t.GetFields(flags);
                    //    Find the matching field
                    for(int i = 0; i < fields.Length; i++) {
                        FieldInfo field = fields[i];
                        
                        //    If we find a match
                        if (fieldName.Equals(field.Name)) {
                            object origValue = field.GetValue(this);
                             //    set the value
                            field.SetValue(this, propertyValue);
                            break;
                        }
                    }
                }
            }
        }
        public void BeginInit() {_isInitializing = true;}
        public void EndInit() {_isInitializing = false;}
    }

RuntimePropertiesSerializer

The RuntimePropertiesSerializer handles the serialization of the the component properties to the generated code. Because we marked any read-only properties that we are managing with the DesignOnly attribute, the default serialization will not attempt to serialize these properties when changes are made to the component properties. We have to handle this code generation. For each read-only property, we generate a call to the InitializeReadonlyProperty call of the RuntimePropertiesBase.

We call the base code serializer first and a let it to handle all the the default serialization. The Serialize routine is called multiple times depending on what code was being generated. From what I could figure out, if the current context of the IDesignerSerializationManager is null, then the component creation code is being generated. If the current context is a CodeTypeDeclaration, then the component initialization code is being generated. We want to generate additional initialization code for the properties we are managing. The base serializer doesn't generate the leading comments if it didn't generate any initialization code, so we should put them in if we need to.

public override object Serialize(
                      IDesignerSerializationManager manager,
                      object value) {
        ... do base serialization ...
        CodeStatementCollection codeStatementCollection = 
                        (CodeStatementCollection)codeObject;
        PropertyDescriptorCollection propertyDescriptorCollection = 
                               TypeDescriptor.GetProperties(value);  
        if (propertyDescriptorCollection.Count > 0 &&
           codeStatementCollection.Count == 0) {
          ... put the component identification comments ...
        }

        CodeFieldReferenceExpression cfre = 
                new CodeFieldReferenceExpression(new 
                    CodeThisReferenceExpression(), component.Site.Name);
        CodeMethodReferenceExpression cmre = 
                new CodeMethodReferenceExpression(cfre, 
                                       INITIALIZATION_METHOD_NAME);

        foreach (PropertyDescriptor pd in propertyDescriptorCollection) {
            if (RuntimePropertyDescriptor.Exists(pd, READ_ONLY_ATTRIBUTE)) {
                CodeExpression valueExpr;
                Type type = pd.PropertyType;
                if (type.IsPrimitive) {
                    valueExpr = new CodePrimitiveExpression(pd.GetValue(value));
                }
                else if (type.IsInstanceOfType("")) {
                    valueExpr = new CodePrimitiveExpression(pd.GetValue(value));
                }
                else if (type.IsEnum) {
                    string expr;
                    if (type.IsNestedPublic) {
                        Type enumType = type.DeclaringType;
                        expr = enumType.FullName + "." + type.Name;
                    }
                    else {
                        expr = type.FullName;
                    }
                    TypeConverter tc = pd.Converter;
                    string name = tcConvertToString(pd.GetValue(value));
                    valueExpr = 
                         new CodeVariableReferenceExpression(expr+"."+name);
                }
                else {
                    TypeConverter tc = pd.Converter;
                    string name = tc.ConvertToString(pd.GetValue(value));
                    valueExpr = new CodeVariableReferenceExpression(name);
                }
                CodeExpression [] cea = new new CodeExpression[] {
                              new CodePrimitiveExpression(pd.DisplayName),
                              valueExpr};
                CodeMethodInvokeExpression codeMethodInvokeExpression = 
                   new CodeMethodInvokeExpression(cmre, cea);

                codeStatementCollection.Add(codeMethodInvokeExpression);
            }
        }
    }
    return codeObject;
}

Most of the work is converting the property value to a string. Intrinsic types and strings are easy. Anything other than enumerated types, we just take out chances with the default type converter defined for the property. Enumerated types are a little more work, because we have to get the fully qualified name of the enumerated type. The type definition could point to the enumerated type class directory or it could point to the class definition that contains the definition. The serializer has to determine which and then build the fully qualified name to the enumerated value.

It turns out that we don't have to do a lot of work for the deseialization, as the framework will actually call the InitializeReadonlyProperty when the component is loaded onto the form at design-time. All we have to do is ensure that the property values are set properly at design time and run time.

public override object Deserialize(
                            IDesignerSerializationManager manager,
                            object codeObject) {
    Type baseType = typeof(RuntimePropertiesBase).BaseType;
    Type codeDomType = typeof(CodeDomSerializer);
    CodeDomSerializer baseSerializer = 
         (CodeDomSerializer)manager.GetSerializer(baseType, codeDomType);

    return baseSerializer.Deserialize(manager, codeObject);
}

RuntimePropertiesDesigner

The RuntimePropertiesDesigner extends ComponentDesigner to provide the extended functionality of the RuntimeProperties. It creates the shadow properties for the designer by searching for any properties that have the ComponentNameListener or UpdateReadOnly attributes. At initialization, it will also hook itself onto the ComponentRename event of the ComponentChangeService. If the component name is changed at design time, it will update all the fields that have been identified with the ComponentNameListener attribute.

internal class RuntimePropertiesDesigner : 
                               ComponentDesigner {
    private static Attribute UPDATE_READ_ONLY = 
                           new UpdateReadOnlyAttribute("");
    private static Attribute COMPONENT_NAME_LISTENER = 
                           new ComponentNameListenerAttribute();

    private ComponentRenameEventHandler _handler;
    private Hashtable                   _properties = null;
    private string                      _siteName;

    public RuntimePropertiesDesigner() {}

    public override void Initialize(IComponent component) {
        base.Initialize(component);
        //    save the name of our component
        _siteName = component.Site.Name;

        IComponentChangeService ccs = 
                   (IComponentChangeService)GetService(
                                   typeof(IComponentChangeService));
        _handler = new ComponentRenameEventHandler(OnComponentRename);
        // Hook the Component Rename event
        ccs.ComponentRename += _handler;
    }

    protected override void Dispose( bool disposing ) {
        IComponentChangeService ccs = 
                   (IComponentChangeService)GetService(
                                   typeof(IComponentChangeService));
        // UnHook from the rename event
        if (_handler != null) {
            ccs.ComponentRename -= _handler;
        }
        base.Dispose( disposing );
    }

    protected override void PreFilterProperties(
                              IDictionary properties) {
        base.PreFilterProperties(properties);
        // Only load the properties the first time
        if (_properties == null) {
            _properties = new Hashtable();
            foreach (PropertyDescriptor pd in properties.Values) {
                bool readOnly = 
                        RuntimePropertyDescriptor.Exists(
                                pd, UPDATE_READ_ONLY);
                // if readonly or related to component name
                // create a design time property
                if (readOnly ||
                    pd.Attributes.Contains(COMPONENT_NAME_LISTENER)) {
                    ArrayList attrs = GetAttributes(pd);
                    // insure the properties are not serialized by
                    // default processing
                    if (readOnly) {
                        attrs.Add(DesignOnlyAttribute.Yes);
                    }

                    _properties[pd.Name] = 
                         RuntimePropertyDescriptor.
                           CreateProperty(typeof(RuntimePropertiesDesigner), 
                                   pd, 
                                   (Attribute[])attrs.ToArray(typeof(Attribute)));
                }
            }
        }

        // copy the design property definitions to the component 
        // definitions replacing any duplicates
        foreach (RuntimePropertyDescriptor mpd in 
                               _properties.Values) {
            properties[mpd.Name] = mpd;
        }
    }

    private ArrayList GetAttributes(PropertyDescriptor pd) {
        ArrayList shadowAttributes = new ArrayList(pd.Attributes);

        foreach (Attribute attr in pd.Attributes) {
            if (attr is ComponentNameListenerAttribute) {
                shadowAttributes.Add(
                       new DefaultValueAttribute(_siteName));
            }
        }

        return shadowAttributes;
    }

    public void OnComponentRename(object sender, 
                                  ComponentRenameEventArgs e) {
        PropertyDescriptorCollection pdc = 
             TypeDescriptor.GetProperties(Component, 
                     new Attribute[] {new ComponentNameListenerAttribute()});
        foreach (PropertyDescriptor pd in pdc) {
            string prop0 = (string)pd.GetValue(Component);
            if (e.OldName.Equals(prop0)) {
                pd.SetValue(Component, e.NewName);
            }
        }
    }
}

RuntimePropertyDescriptor

RuntimePropertyDescriptor class provides the property descriptor container for the shadow properties that are created and maintained by the designer. It extends the PropertyDescriptor class to provide the functionality to store the design time value, and when created, to set the default value to the value supplied in the DefaultValue attribute. The SetValue method hooks any changes to the value into the Visual Studio component change notification so that changes appear in the property windows in a timely manner.

public override void SetValue(object component, object Value) {
    if (component == null) {
        return;
    }

    ISite site = RuntimePropertyDescriptor.GetSite(component);
    IComponentChangeService ccs = null;
    object oldValue = _propertyValue;
    if (this.IsReadOnly) {
        return;
    }
    if (site != null) {
        ccs = (IComponentChangeService)
                        site.GetService(typeof(IComponentChangeService));
    }
    if (ccs != null) {
        try {
            ccs.OnComponentChanging(component, this);
        }
        catch (CheckoutException exception1) {
            if (exception1 != CheckoutException.Canceled) {
                throw exception1;
            }
            return;
        }
    }
    try {
        _propertyValue = Value;
        this.OnValueChanged(component, EventArgs.Empty);
    }
    catch (Exception exception2) {
        if ((exception2 is TargetInvocationException) && 
            (exception2.InnerException != null)) {
            throw exception2.InnerException;
        }
        throw exception2;
    }
    finally {
        if (ccs != null) {
            ccs.OnComponentChanged(component, this, oldValue, Value);
        }
    }
}

Test Component Design

To test the approach, I designed a component with the following properties:

Properties

  • PublicEnumeratedProperty: This property is a read-only runtime property of a public enumerated type.
  • NestedEnumeratedProperty: This property is a read-only runtime property of a public enumerated type nested within the MyComponent class.
  • MenuItem: This property is a read-only runtime property, which specifies a connection to a menu item.
  • Property0: This property is a read-only runtime property. Additionally, this property will have an initial value of the name of the component. If the name of the component changes, then the value of this property should change as well.
  • Property1: This property is a read-only runtime property.
  • Property2: This property is a read-only runtime property.
  • VisibleProperty: This property is read/write at both design and runtime.

Create The Component

Creating the component is straight forward. Visual Studio creates the basic component class and we can then add the read-only properties.

namespace Almdal.MyComponent {
    public enum PublicEnumType {Public1, Public2};

    public class MyComponent : RuntimePropertiesBase {
        public enum NestedEnumType {Nested1, Nested2};

        private string        _prop0 = "";
        private bool          _prop1 = false;
        private int           _prop2 = 0;
        private string        _prop = "";
        private MenuItem      _item =null;
        private EnumProperty  _enum = EnumProperty.Value1;
        private EnumProperty2 _enum2 = EnumProperty2.Value1;

        public MyComponent(IContainer container) {
            container.Add(this);
            InitializeComponent();
        }

        [DefaultValue(PublicEnumType.Public1)]
        public PublicEnumType PublicEnumeratedProperty {
            get {return _enum2;}
        }

        [DefaultValue(NestedEnumType.Nested)]
        public NestedEnumType NestedEnumeratedProperty {
            get {return _enum;}
        }

        public MenuItem MenuItem {
            get {return _item;}
        }

        public string Property0 {
            get {return _prop0;}
        }

        [DefaultValue(false)]
        public bool Property1 {
            get {return _prop1;}
        }

        [DefaultValue(0)]
        public int Property2 {
            get {return _prop2;}
        }

        public string VisibleProperty {
            get {return _prop;}
            set {_prop = value;}
        }
    }
}

When this component is dropped on to a form in Visual Studio, we find that the properties that are supposed to be read-only at runtime are also read-only at design time. This result is somewhat expected, but definitely not the result we are looking for.

Read-Only Properties Learn to write at Design time

Adding the UpdateReadOnly attribute will allow the the RuntimePropertiesDesigner to manage the read only properties and allows us to change the values in Visual Studio. The parameter to the UpdateReadOnly attribute is the name of the field that the property value is stored in.

namespace Almdal.MyComponent {
    public enum PublicEnumType {Public1, Public2};

    public class MyComponent : RuntimePropertiesBase {
        public enum NestedEnumType {Nested1, Nested2};

        private string        _prop0 = "";
        private bool          _prop1 = false;
        private int           _prop2 = 0;
        private string        _prop = "";
        private MenuItem      _item =null;
        private EnumProperty  _enum = EnumProperty.Value1;
        private EnumProperty2 _enum2 = EnumProperty2.Value1;

        public MyComponent(IContainer container) {
            container.Add(this);
            InitializeComponent();
        }

        [UpdateReadOnly("_enum2"),
         DefaultValue(PublicEnumType.Public1)]
        public PublicEnumType PublicEnumeratedProperty {
            get {return _enum2;}
        }

        [UpdateReadOnly("_enum"),
         DefaultValue(NestedEnumType.Nested)]
        public NestedEnumType NestedEnumeratedProperty {
            get {return _enum;}
        }

        [UpdateReadOnly("_item")]
        public MenuItem MenuItem {
            get {return _item;}
        }

        [UpdateReadOnly("_prop0")]
        public string Property0 {
            get {return _prop0;}
        }

        [UpdateReadOnly("_prop1"),
         DefaultValue(false)]
        public bool Property1 {
            get {return _prop1;}
        }

        [UpdateReadOnly("_prop2"),
         DefaultValue(0)]
        public int Property2 {
            get {return _prop2;}
        }

        public string VisibleProperty {
            get {return _prop;}
            set {_prop = value;}
        }
    }
}

When we test the component, we find we are a bit closer to what we want. The properties can be modified, but when we changed the component name, Property0 or VisibleProperty doesn't change.

It is also worth noting that we are generating the following code as part of the form initialization:

// 
// thisComponent
// 
this.thisComponent.VisibleProperty = "myComponent";
this.thisComponent.InitializeReadonlyProperty("Property1", false);
this.thisComponent.InitializeReadonlyProperty("Property0", "thisComponent");
this.thisComponent.InitializeReadonlyProperty("Property2", 0);
this.thisComponent.InitializeReadonlyProperty("NestedEnumeratedProperty", 
                        Almdal.MyComponent.MyComponent.NestedEnumType.Nested1);
this.thisComponent.InitializeReadonlyProperty("PublicEnumeratedProperty", 
                        Almdal.MyComponent.PublicEnumType.Public1);
this.thisComponent.InitializeReadonlyProperty("MenuItem", menuItem1);

Our Component Learns to Listen

If we add the ComponentNameListener attribute to the properties that need notification, these properties will change if the component name changes.

public class MyComponent : RuntimePropertiesBase {
             ...
        [ComponentNameListener,
         UpdateReadOnly("_prop0")]
        public string Property0 {
            get {return _prop0;}
        }
        ...
        [ComponentNameListener]
        public string VisibleProperty {
            get {return _prop;}
            set {_prop = value;}
        }
    }
}

Now, when we add it to form1 and change the component name, Property0 is kept updated.

Review

We set out to implement a component that had read only properties at runtime while not exposing a set method, and we achieved this goal by creating a custom designer for the component.

We wanted to create a component that could provide this functionality in a generalized and resuable manner, and the RuntimeProperties does this.

We wanted to deserialize the design time values into the runtime, and we accomplished that by creating a custom code serializer that generated a call to a specialized method on the RuntimePropertiesBase class.

And finally, we wanted to get a compile error if the developer tried to assign a value to this property during development. And this works as well. An attempt to assign a value to Property0 results in an error message (Property or indexer 'TestSerialization.MyComponent.Property0' cannot be assigned to -- It is read only).

Article History

  • 1.1 - Jan 31st, 2005
    • Created a base component to facilitate reuse by implementing the concepts presented in this article.
  • 1.0 - Nov 22nd, 2004
    • Initial version.

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

Share

About the Author

Tim Almdal

Canada Canada
No Biography provided

Comments and Discussions

 
GeneralMy vote of 5 Pin
kitkarsons14-Feb-13 18:24
memberkitkarsons14-Feb-13 18:24 

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

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

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.150520.1 | Last Updated 3 Feb 2005
Article Copyright 2004 by Tim Almdal
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid