- Components
- Design-Time Integration Basics
- Extender Property Providers
- Type Converters
- UI Type Editors
- Custom Designers
- Where Are We?
Because a component is a class that's made to be integrated into a design-time host, it has a life separate from the run-time mode that we normally think of for objects. It's not enough for a component to do a good job when interacting with a user at run time as per developer instructions; a component also needs to do a good job when interacting with the developer at design time.
Hosts, Containers, and Sites
In Visual Studio .NET, the Windows Forms Designer is responsible for providing design-time services during Windows Forms development. At a high level, these services include a form's UI and code views. The responsibility of managing integration between design-time objects and the designer is handled by the designer's internal implementation of IDesignerHost (from the System.ComponentModel.Design namespace). The designer host stores IComponent references to all design-time objects on the current form and also stores the form itself (which is also a component). This collection of components is available from the IDesignerHost interface through the Container property of type IContainer (from the System.ComponentModel namespace):
interface IContainer : IDisposable { ComponentCollection Components { get; } void Add(IComponent component); void Add(IComponent component, string name); void Remove(IComponent component); }
This implementation of IContainer allows the designer host to establish a relationship that helps it manage each of the components placed on the form. Contained components can access the designer host and each other through their container at design time. Figure 9.5 illustrates this two-way relationship.
Figure 9.5. Design-Time Architecture
In Figure 9.5 you can see that the fundamental relationship between the designer host and its components is established with an implementation of the ISite interface (from the System.ComponentModel namespace):
interface ISite : IServiceProvider { IComponent Component { get; } IContainer Container { get; } bool DesignMode { get; } string Name { get; set; } }
Internally, a container stores an array of sites. When each component is added to the container, the designer host creates a new site, connecting the component to its design-time container and vice versa by passing the ISite interface in the IComponent.Site property implementation:
interface IComponent : IDisposable { ISite Site { get; set; } event EventHandler Disposed; }
The Component base class implements IComponent and caches the site's interface in a property. It also provides a helper property to go directly to the component's container without having to go first through the site:
class Component : MarshalByRefObject, IComponent, IDisposable { public IContainer Container { get; } public virtual ISite Site { get; set; } protected bool DesignMode { get; } protected EventHandlerList Events { get; } }
The Component base class gives a component direct access to both the container and the site. A component can also access the Visual Studio .NET designer host itself by requesting the IDesignerHost interface from the container:
IDesignerHost designerHost = this.Container as IDesignerHost;
In Visual Studio .NET, the designer has its own implementation of the IDesignerHost interface, but, to fit into other designer hosts, it's best for a component to rely only on the interface and not on any specific implementation.
Debugging Design-Time Functionality
To demonstrate the .NET Framework's various design-time features and services, I've built a sample.3 Because components and controls share the same design-time features and because I like things that look snazzy, I built a digital/analog clock control with the following public members:
public class ClockControl : Control { public ClockControl(); public DateTime Alarm { get; set; } public bool IsItTimeForABreak { get; set; } public event AlarmHandler AlarmSounded; ... }
Figure 9.6 shows the control in action.
Figure 9.6. Snazzy Clock Control
When you build design-time features into your components,4 you'll need to test them and, more than likely, debug them. To test run-time functionality, you simply set a breakpoint in your component's code and run a test application, relying on Visual Studio .NET to break at the right moment.
What makes testing design-time debugging different is that you need a design-time host to debug against; an ordinary application won't do. Because the hands-down hosting favorite is Visual Studio .NET itself, this means that you'll use one instance of Visual Studio .NET to debug another instance of Visual Studio .NET with a running instance of the component loaded. This may sound confusing, but it's remarkably easy to set up:
-
Open the component solution to debug in one instance of Visual Studio .NET.
-
Set a second instance of Visual Studio .NET as your debug application by going to Project | Properties | Configuration Properties | Debugging and setting the following properties:
-
Set Debug Mode to Program.
-
Set Start Application to <your devenv.exe path>\devenv.exe.
-
Set Command Line Arguments to <your test solution path>\yourTestSolution.sln.
-
-
Choose Set As StartUp Project on your component project.
-
Set a breakpoint in the component.
-
Use Debug | Start (F5) to begin debugging.
At this point, a second instance of Visual Studio.NET starts up with another solution, allowing you to break and debug at will, as illustrated in Figure 9.7.
Figure 9.7. Design-Time Control Debugging
The key to making this setup work is to have one solution loaded in one instance of VS.NET that starts another instance of VS.NET with a completely different solution to test your component in design mode.
The DesignMode Property
To change the behavior of your component at design time, often you need to know that you're running in a Designer. For example, the clock control uses a timer component to track the time via its Tick event handler:
public class ClockControl : Control { ... Timer timer = new Timer(); ... public ClockControl() { ... // Initialize timer timer.Interval = 1000; timer.Tick += new System.EventHandler(this.timer_Tick); timer.Enabled = true; } ... void timer_Tick(object sender, EventArgs e) { // Refresh clock face this.Invalidate(); ... } }
Inspection reveals that the control is overly zealous in keeping time both at design time and at run time. Such code should really be executed at run time only. In this situation, a component or control can check the DesignMode property, which is true only when it is executing at design time. The timer_Tick event handler can use DesignMode to ensure that it is executed only at run time, returning immediately from the event handler otherwise:
void timer_Tick(object sender, EventArgs e) { // Don't execute event if running in design time if( this.DesignMode ) return; this.Invalidate(); ... }
Note that the DesignMode property should not be checked from within the constructor or from any code that the constructor calls. A constructor is called before a control is sited, and it's the site that determines whether or not a control is in design mode. DesignMode will also be false in the constructor.
Attributes
Design-time functionality is available to controls in one of two ways: programmatically and declaratively. Checking the DesignMode property is an example of the programmatic approach. One side effect of using a programmatic approach is that your implementation takes on some of the design-time responsibility, resulting in a blend of design-time and run-time code within the component implementation.
The declarative approach, on the other hand, relies on attributes to request design-time functionality implemented somewhere else, such as the designer host. For example, consider the default Toolbox icon for a component, as shown in Figure 9.8.
Figure 9.8. Default Toolbox Icon
If the image is important to your control, you'll want to change the icon to something more appropriate. The first step is to add a 16×16, 16-color icon or bitmap to your project and set its Build Action to Embedded Resource (embedded resources are discussed in Chapter 10: Resources). Then add the ToolboxBitmapAttribute to associate the icon with your component:
[ToolboxBitmapAttribute( typeof(ClockControlLibrary.ClockControl), "images.ClockControl.ico")] public class ClockControl : Control {...}
The parameters to this attribute specify the use of an icon resource located in the "images" project subfolder.
You'll find that the Toolbox image doesn't change if you add or change ToolboxBitmapAttribute after the control has been added to the Toolbox. However, if your implementation is a component, its icon is updated in the component tray. One can only assume that the Toolbox is not under the direct management of the Windows Form Designer, whereas the component tray is. To refresh the Toolbox, remove your component and then add it again to the Toolbox. The result will be something like Figure 9.9.
Figure 9.9. New and Improved Toolbox Icon
You can achieve the same result without using ToolboxBitmapAttribute: Simply place a 16×16, 16-color bitmap in the same project folder as the component, and give it the same name as the component class. This is a special shortcut for the ToolboxBitmapAttribute only; don't expect to find similar shortcuts for other design-time attributes.
Property Browser Integration
No matter what the icon is, after a component is dragged from the Toolbox onto a form, it can be configured through the designer-managed Property Browser. The Designer uses reflection to discover which properties the design-time control instance exposes. For each property, the Designer calls the associated get accessor for its current value and renders both the property name and the value onto the Property Browser. Figure 9.10 shows how the Property Browser looks for the basic clock control.
Figure 9.10. Visual Studio.NET with a Clock Control Chosen
The System.ComponentModel namespace provides a comprehensive set of attributes, shown in Table 9.1, to help you modify your component's behavior and appearance in the Property Browser.
By default, public read and read/write properties—such as the Alarm property highlighted in Figure 9.10—are displayed in the Property Browser under the "Misc" category. If a property is intended for run time only, you can prevent it from appearing in the Property Browser by adorning the property with BrowsableAttribute:
[BrowsableAttribute(false)] public bool IsItTimeForABreak { get { ... } set { ... } }
With IsItTimeForABreak out of the design-time picture, only the custom Alarm property remains. However, it's currently listed under the Property Browser's Misc category and lacks a description. You can improve the situation by applying both CategoryAttribute and DescriptionAttribute:
[ CategoryAttribute("Behavior"), DescriptionAttribute("Alarm for late risers") ] public DateTime Alarm { get { ... } set { ... } }
Table 9.1. Design-Time Property Browser Attributes
Attribute |
Description |
---|---|
AmbientValueAttribute |
Specifies the value for this property that causes it to acquire its value from another source, usually its container (see the section titled Ambient Properties in Chapter 8: Controls). |
BrowsableAttribute |
Determines whether the property is visible in the Property Browser. |
CategoryAttribute |
Tells the Property Browser which group to include this property in. |
DescriptionAttribute |
Provides text for the Property Browser to display in its description bar. |
DesignOnlyAttribute |
Specifies that the design-time value of this property is serialized to the form's resource file. This attribute is typically used on properties that do not exist at run time. |
MergablePropertyAttribute |
Allows this property to be combined with properties from other objects when more than one are selected and edited. |
ParenthesizePropertyNameAttribute |
Specifies whether this property should be surrounded by parentheses in the Property Browser. |
ReadOnlyAttribute |
Specifies that this property cannot be edited in the Property Browser. |
After adding these attributes and rebuilding, you will notice that the Alarm property has relocated to the desired category in the Property Browser, and the description appears on the description bar when you select the property (both shown in Figure 9.11). You can actually use CategoryAttribute to create new categories, but you should do so only if the existing categories don't suitably describe a property's purpose. Otherwise, you'll confuse users looking for your properties in the logical category.
Figure 9.11. Alarm Property with CategoryAttribute and DescriptionAttribute Applied
In Figure 9.11, some property values are shown in boldface and others are not. Boldface values are those that differ from the property's default value, which is specified by DefaultValueAttribute:
[ CategoryAttribute("Appearance"), DescriptionAttribute("Whether digital time is shown"), DefaultValueAttribute(true) ] public bool ShowDigitalTime { get { ... } set { ... } }
Using DefaultValueAttribute also allows you to reset a property to its default value using the Property Browser, which is available from the property's context menu, as shown in Figure 9.12.
Figure 9.12. Resetting a Property to Its Default Value
This option is disabled if the current property is already the default value. Default values represent the most common value for a property. Some properties, such as Alarm or Text, simply don't have a default that's possible to define, whereas others, such as Enabled and ControlBox, do.
Just like properties, a class can have defaults. You can specify a default event by adorning a class with DefaultEventAttribute:
[DefaultEventAttribute("AlarmSounded")] class ClockControl : Control { ... }
Double-clicking the component causes the Designer to automatically hook up the default event; it does this by serializing code to register with the specified event in InitializeComponent and providing a handler for it:
class ClockControlHostForm : Form { ... void InitializeComponent() { ... this.clockControl1.AlarmSounded += new AlarmHandler(this.clockControl1_AlarmSounded); ... } ... void clockControl1_AlarmSounded( object sender, ClockControlLibrary.AlarmType type) { } ... }
You can also adorn your component with DefaultPropertyAttribute:
[DefaultPropertyAttribute("ShowDigitalTime")] public class ClockControl : Windows.Forms.Control { ... }
This attribute causes the Designer to highlight the default property when the component's property is first edited, as shown in Figure 9.13.
Figure 9.13. Default Property Highlighted in the Property Browser
Default properties aren't terribly useful, but setting the correct default event properly can save a developer's time when using your component.
Code Serialization
Whereas DefaultEventAttribute and DefaultPropertyAttribute affect the behavior only of the Property Browser, DefaultValueAttribute serves a dual purpose: It also plays a role in helping the Designer determine which code is serialized to InitializeComponent. Properties that don't have a default value are automatically included in InitializeComponent. Those that have a default value are included only if the property's value differs from the default. To avoid unnecessarily changing a property, your initial property values should match the value set by DefaultValueAttribute.
DesignerSerializationVisibilityAttribute is another attribute that affects the code serialization process. The DesignerSerializationVisibilityAttribute constructor takes a value from the DesignerSerializationVisibility enumeration:
enum DesignerSerializationVisibility { Visible, // initialize this property if nondefault value Hidden, // don't initialize this property Content // initialize sets of properties on a subobject }
The default, Visible, causes a property's value to be set in InitializeComponent if the value of the property is not the same as the value of the default. If you'd prefer that no code be generated to initialize a property, use Hidden:
[ DefaultValueAttribute(true), DesignerSerializationVisibilityAttribute( DesignerSerializationVisibility.Hidden) ] public bool ShowDigitalTime { get { ... } set { ... } }
You can use Hidden in conjunction with BrowsableAttribute set to false for run-time-only properties. Although BrowsableAttribute determines whether a property is visible in the Property Browser, its value may still be serialized unless you prevent that by using Hidden.
By default, properties that maintain a collection of custom types cannot be serialized to code. Such a property is implemented by the clock control in the form of a "messages to self" feature, which captures a set of messages and displays them at the appropriate date and time. To enable serialization of a collection, you can apply DesignerSerializationVisibility. Content to instruct the Designer to walk into the property and serialize its internal structure:
[ CategoryAttribute("Behavior"), DescriptionAttribute ("Stuff to remember for later"), DesignerSerializationVisibilityAttribute (DesignerSerializationVisibility.Content) ] public MessageToSelfCollection MessagesToSelf { get { ... } set { ... } }
The generated InitializeComponent code for a single message looks like this:
void InitializeComponent() { ... this.clockControl1.MessagesToSelf.AddRange( new ClockControlLibrary.MessageToSelf[] { new ClockControlLibrary.MessageToSelf( new System.DateTime(2003, 2, 22, 21, 55, 0, 0), "Wake up")}); ... }
This code also needs a "translator" class to help the Designer serialize the code to construct a MessageToSelf type. This is covered in detail in the section titled "Type Converters" later in this chapter.
Host Form Integration
While we're talking about affecting code serialization, there's another trick that's needed for accessing a component's hosting form. For example, consider a clock control and a clock component, both of which offer the ability to place the current time in the hosting form's caption. Each needs to acquire a reference to the host form to set the time in the form's Text property. The control comes with native support for this requirement:
Form hostingForm = this.Parent as Form;
Unfortunately, components do not provide a similar mechanism to access their host form. At design time, the component can find the form in the designer host's Container collection. However, this technique will not work at run time because the Container is not available at run time. To get its container at run time, a component must take advantage of the way the Designer serializes code to the InitializeComponent method. You can write code that takes advantage of this infrastructure to seed itself with a reference to the host form at design time and run time. The first step is to grab the host form at design time using a property of type Form:
Form hostingForm = null; [BrowsableAttribute(false)] public Form HostingForm { // Used to populate InitializeComponent at design time get { if( (hostingForm == null) && this.DesignMode ) { // Access designer host and obtain reference to root component IDesignerHost designer = this.GetService(typeof(IDesignerHost)) as IDesignerHost; if( designer != null ) { hostingForm = designer.RootComponent as Form; } } return hostingForm; } set {...} }
The HostingForm property is used to populate the code in InitializeComponent at design time, when the designer host is available. Stored in the designer host's RootComponent property, the root component represents the primary purpose of the Designer. For example, a Form component is the root component of the Windows Forms Designer. DesignerHost.RootComponent is a helper function that allows you to access the root component without enumerating the Container collection. Only one component is considered the root component by the designer host. Because the HostingForm property should go about its business transparently, you should decorate it with BrowsableAttribute set to false, thereby ensuring that the property is not editable from the Property Browser.
Because HostForm is a public property, the Designer retrieves HostForm's value at design time to generate the following code, which is needed to initialize the component:
void InitializeComponent() { ... this.myComponent1.HostingForm = this; ... }
At run time, when InitializeComponent runs, it will return the hosting form to the component via the HostingForm property setter:
Form hostingForm = null; [BrowsableAttribute(false)] public Form HostingForm { get { ... } // Set by InitializeComponent at run time set { if( !this.DesignMode ) { // Don't change hosting form at run time if( (hostingForm != null) && (hostingForm != value) ) { throw new InvalidOperationException ("Can't set HostingForm at run time."); } } else hostingForm = value; } }
In this case, we're using our knowledge of how the Designer works to trick it into handing our component a value at run-time that we pick at design-time.
Batch Initialization
As you may have noticed, the code that eventually gets serialized to InitializeComponent is laid out as an alphanumerically ordered sequence of property sets, grouped by object. Order isn't important until your component exposes range-dependent properties, such as Min/Max or Start/Stop pairs. For example, the clock control also has two dependent properties: PrimaryAlarm and BackupAlarm (the Alarm property was split into two for extra sleepy people).
Internally, the clock control instance initializes the two properties 10 minutes apart, starting from the current date and time:
DateTime primaryAlarm = DateTime.Now; DateTime backupAlarm = DateTime.Now.AddMinutes(10);
Both properties should check to ensure that the values are valid:
public DateTime PrimaryAlarm { get { return primaryAlarm; } set { if( value >= backupAlarm ) throw new ArgumentOutOfRangeException ("Primary alarm must be before Backup alarm"); primaryAlarm = value; } } public DateTime BackupAlarm { get { return backupAlarm; } set { if( value < primaryAlarm ) throw new ArgumentOutOfRangeException ("Backup alarm must be after Primary alarm"); backupAlarm = value; } }
With this dependence checking in place, at design time the Property Browser will show an exception in an error dialog if an invalid property is entered, as shown in Figure 9.14.
Figure 9.14. Invalid Value Entered into the Property Browser
This error dialog is great at design time, because it lets the developer know the relationship between the two properties. However, there's a problem when the properties are serialized into InitializeComponent alphabetically:
void InitializeComponent() { ... // clockControl1 this.clockControl1.BackupAlarm = new System.DateTime(2003, 11, 24, 13, 42, 47, 46); ... this.clockControl1.PrimaryAlarm = new System.DateTime(2003, 11, 24, 13, 57, 47, 46); ... }
Notice that even if the developer sets the two alarms properly, as soon as BackupAlarm is set and is checked against the default value of PrimaryAlarm, a run-time exception will result.
To avoid this, a component must be notified when its properties are being set from InitializeComponent in "batch mode" so that they can be validated all at once at the end. Implementing the ISupportInitialize interface (from the System.ComponentModel namespace) provides this capability, with two notification methods to be called before and after initialization:
public interface ISupportInitialize { public void BeginInit(); public void EndInit(); }
When a component implements this interface, calls to BeginInit and EndInit are serialized to InitializeComponent:
void InitializeComponent() { ... ((System.ComponentModel.ISupportInitialize) (this.clockControl1)).BeginInit(); ... // clockControl1 this.clockControl1.BackupAlarm = new System.DateTime(2003, 11, 24, 13, 42, 47, 46); ... this.clockControl1.PrimaryAlarm = new System.DateTime(2003, 11, 24, 13, 57, 47, 46); ... ((System.ComponentModel.ISupportInitialize) (this.clockControl1)).EndInit(); ... }
The call to BeginInit signals the entry into initialization batch mode, a signal that is useful for turning off value checking:
public class ClockControl : Control, ISupportInitialize { ... bool initializing = false; ... void BeginInit() { initializing = true; } ... public DateTime PrimaryAlarm { get { ... } set { if( !initializing ) { /* check value */ } primaryAlarm = value; } } public DateTime BackupAlarm { get { ... } set { if( !initializing ) { /* check value */ } backupAlarm = value; } } }
Placing the appropriate logic into EndInit performs batch validation:
public class ClockControl : Control, ISupportInitialize { void EndInit() { if( primaryAlarm >= backupAlarm ) throw new ArgumentOutOfRangeException ("Primary alarm must be before Backup alarm"); } ... }
EndInit also turns out to be a better place to avoid the timer's Tick event, which currently fires once every second during design time. Although the code inside the Tick event handler doesn't run at design time (because it's protected by a check of the DesignMode property), it would be better not to even start the timer at all until run time. However, because DesignMode can't be checked in the constructor, a good place to check it is in the EndInit call, which is called after all properties have been initialized at run time or at design time:
public class ClockControl : Control, ISupportInitialize { ... void EndInit() { ... if( !this.DesignMode ) { // Initialize timer timer.Interval = 1000; timer.Tick += new System.EventHandler(this.timer_Tick); timer.Enabled = true; } } }
The Designer and the Property Browser provide all kinds of design-time help to augment the experience of developing a component, including establishing how a property is categorized and described to the developer and how it's serialized for the InitializeComponent method.