Adding .NET Control Properties

Dr. Dobb's Journal March 2004

Avoid pitfalls when adding properties to your custom controls

By Phil Wright

Phil specializes in developing user interfaces and custom controls in C# for .NET projects. Founder of Crownwood Consulting and author of the popular DotNetMagic user interface library, he can be contacted at phil.wright@dotnetmagic.com.

When I started writing my first .NET custom control, I thought that adding properties would be trivial. It certainly started out simple enough. All I needed to do was add a private field for storage, then write the get and set property accessors. Drop an instance of my new control onto a Form and—presto!—my property was available in the property browser. Compared to the old days of writing ActiveX controls and adding IDL definitions by hand, this was Nirvana.

Still, although the property worked, it did not integrate well into the property browser at design time; see Figure 1. It has been positioned under a category called "Misc" rather than one of the existing categories, such as Behavior or Appearance. No useful description is displayed in the panel under the property, and notice how the value of the property is shown in bold when the others are not. When the property is right-clicked, the context menu has a Reset option but this is disabled and prevents users setting the value from setting it back to its default.

This lack of integration with the design-time environment actually highlights one of the great strengths of the .NET Framework. Almost every feature in .NET is exposed in a progressive manner, with control properties being no exception. Consequently, you can choose the level of sophistication required and balance this against the time needed to achieve it.

After writing several professional standard controls, you quickly realize two things:

For a simple control used purely for internal purposes, you can add a property in a matter of seconds by adding a private field and the appropriate get/set accessors. If you are writing a professional control, then you can spend the additional time needed to add all those extra bells and whistles. Discovering how to add those extras requires a little investigation, as the process is not described in any single document.

In this article, I present an idiom to use when dealing with control properties that not only improves the quality of the produced code, but also ensures a consistent and professional feel for control users. And in the process, you can add those bells and whistles.

Step 1: Storage and Access

Most properties are going to need a field for storage. You should always define this with private access to ensure your class adheres to the principle of encapsulation. It is tempting to declare the field as protected so that derived classes can gain direct access to the field. Although this appears more efficient because the derived class does not need to get through the public get accessor, it breaks the principle of encapsulation. If you decide to change the implementation in the future so that the return value is calculated, then your derived class won't work as expected. Once your field has been declared, you just need to write a simple public property (as in Example 1) that gets and sets the value.

Step 2: Initialization

The next step is to ensure the field is initialized by the control. The obvious solution is to just set the field to an appropriate value in the control constructor. In the case of a property exposed by a control, this is not quite adequate because you need to take into account an extra requirement. If you right-click a property in the property browser, you notice the context menu has the option to Reset the property value. As a well-behaved property, you want to ensure any properties you add take advantage of this ability.

Rather than have two places in the control where the field value is defined, you should ensure it happens in only a single place. Otherwise, you are just asking for someone to modify the default value in one place and forget to update the other. So the constructor is going to make a call to the method that implements the reset functionality that is called by the property browser.

All you need to do is add a public method with the name ResetTitle (Example 2). Any exposed property with a name XXX simply needs a ResetXXX method and the property browser will automatically find and use it.

Step 3: Property-Browser Friendly

The third and last of the basic steps is to provide all the information needed by the property browser so can you integrate it as fully as possible. First off, you need to indicate where inside the browser the property should be listed. By default, it is placed in the Misc category.

Whenever possible, it is best to place a property inside one of the existing categories so that users find it easy to locate. In this example, the property is used to affect the appearance, so it should be placed in the Appearance category. You also need to supply a brief description so users can quickly decide what the property does.

Last of all, you should provide a default value. The browser uses this to decide if the property value should be displayed in bold or normal font. When the value is identical to the default value, it is displayed in a normal font. If you change the value, then it is shown in bold. Until you define a default value for the property, it is always shown in bold. It also has another side effect. Code generation on a Form only occurs for those properties that are not equal to the default value. Obviously, there would be no point in generating code to set a property to the value it already has by default. So providing the default also reduces the amount of generated code. Figure 2 provides a more professional feel and shows the Reset context menu option enabled because the value has been changed from its default value; Example 3.

Step 4: Change Notifications

This is an optional step for properties you want to notify that have changed in value. Usually, this is the case for properties that can be a target for data binding, so any change in the value needs to be notified so that the data source can be updated with the new value. However, do not feel that the property must be an obvious target for binding before you add property change notifications. If users might be interested in when the value changes, then add the notification.

If you use the property browser for a TextBox control and list the events it exposes, you notice a large number under the category of Property Changed; see Example 4(a). From this, you see the naming convention used is the name of the property and postfix Changed. All property change events expose themselves with the EventHandler delegate signature and remember to add Category and Description attributes to the event definition in the same manner as for the associated property.

Now a routine is needed that raises the event. By convention, this should be a protected virtual method so that derived classes can override the implementation and add additional processing; see Example 4(b).

Last of all, change the set implementation of the property so that it calls the internal routine but only when the value has changed; see Example 4(c). This is important, as setting the value to its existing value should not cause an event to be fired.

These four steps make up the basic idiom for adding any new property to your control. Step 4 is optional but likely to be applied in a significant number of cases. So what initially seems like a trivial process ends up producing the code in Listing One.

Advanced Idiom

A calculated property does not have any private field for storage because the value is recalculated each time it is requested. In this case, the property would not have a set accessor and would not require either a DefaultValue or a ResetXXX function. In fact, you would not want the property to appear in the property browser at all. Thus, you would add the Browsable(false) attribute to the property so it does not appear in browser.

Of more interest are dependent properties because they still have a private field for storage but where the default value varies. This can happen when the property is dependent on the value of another property to determine an appropriate default. In this case, you cannot use the DefaultValue attribute as it is not correct in all cases, but you can still implement the ResetXXX function to ensure it defines the value appropriately.

To ensure the property correctly takes part in code generation, you need to implement an additional method that is used by the designer environment. This method takes the form ShouldSerializeXXX and returns a bool value that indicates if the property should be saved, and being a method you can perform whatever calculation is needed; see Example 5.

Dependent Initialization

Deciding when to persist into generated code is now the only complication of dependent properties. You also need to consider the order in which properties are set by the generated code. Properties are persisted into the generated code in alphabetical order.

Assume you have two interrelated Mode and Auto properties. Whenever the Mode value is changed, it automatically sets the Auto property to match with the same value. In practice, this style of behavior is useful if a control has a large number of properties, where setting a new mode value causes all other properties to default to an appropriate value for that mode. Then users only need to change the few properties that do not need to be the default for that mode.

The problem emerges because property Auto is generated in code before the Mode. So the setting of the Mode then overwrites any value already assigned to Auto because setting the Mode resets the value of Auto back to the appropriate default. Anticipating just such a situation, the .NET Framework has an interface called ISupportInitialize that should be implemented by any user control that needs to handle such property dependencies.

When this interface is detected by the design-time code generation, it adds calls to BeginInit and EndInit around the setting of the properties. Now your control can detect the start and/or finish of the property setup. You can leverage the implementation of this interface because, according to the idiom, you have a list of ResetXXX calls in your construction to define the initial state of the control. To avoid the same problem as that from generated code, you might as well place calls to the same BeginInit and EndInit in the constructor.

You can see in Listing Two, which implements this (with standard code omitted for clarity), that the additional field _defaultAuto is used to detect whether the Auto property should be in the default state at the end of the initialization sequence. If so, then it calls the ResetAuto method to ensure it is in the correct state.

DDJ

Listing One

public class MyControl : UserControl
{
    private string _title;

    [Category("Property Changed")]
    [Description("Fired when title is changed.")]
    public event EventHandler TitleChanged;

    public MyControl()
    {
        ResetTitle();
    }
    [Category("Appearance")]
    [Description("Text displayed in the control title.")]
    [DefaultValue("MyDocument")]
    public string Title
    {
        get { return _title; }
        set 
        { 
            if (_title != value)
            {
                _title = value; 
                OnTitleChanged(EventArgs.Empty);
            }
        }
    }
    public void ResetTitle()
    {
        Title = "MyDocument";
    }
    protected virtual void OnTitleChanged(EventArgs e)
    {
        if (TitleChanged != null)
            TitleChanged(this, e);
    }
}

Back to Article

Listing Two

public class MyControl : UserControl, ISupportInitialize
{
    private bool _auto;
    private bool _mode;
    private bool _defaultAuto;
    public MyControl()
    {
        BeginInit();
        ResetAuto();
        ResetMode();
        EndInit();
    }
    public void BeginInit()
    {
    _defaultAuto = false;
    }
    public void EndInit()
    {
        if (_defaultAuto)
            ResetAuto();
    }
    [Category("Behavior")]
    [Description("Mode of operation.)]
    [DefaultValue(true)]
    public bool Mode
    {
        get { return _mode; }
        set
        {
            if (_mode != value)
            {
                _mode = value;
                Auto = !value;
    }
    }
        }   
    public void ResetMode()
    {
    Mode = true;
}
    [Category("Behavior")]
    [Description("Automatic reset from Mode.)]
    public bool Auto
    {
        get { return _auto; }   
        set 
        {
            if (_auto != value)
                _auto = value;
    
            _defaultAuto = false;
           }
    }
    public void ResetAuto()
    {
        if (Mode)
            Auto = false;
        else 
            Auto = true;
        _defaultAuto = true;
    }
    private bool ShouldSerializeAuto()
    {
    return (Auto == Mode);
}
    }

Back to Article