Design-Time Integration Basics


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 VS.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   Inherits IDisposable   ReadOnly Property Components() As ComponentCollection   Overloads Sub Add(component As IComponent)   Overloads Sub Add(component As IComponent, name As String)   Sub Removed(component As IComponent) End Interface 

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   Inherits IServiceProvider   ReadOnly Property Component() As IComponent   ReadOnly Property Container() As IContainer   ReadOnly Property DesignMode() As Boolean   Property Name() As String End Interface 

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   Inherits IDisposable   Property Site() As ISite   Event Disposed As EventHandler End Interface 

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:

 
 Public Class Component   Inherits MarshalByRefObject   Implements IComponent   Implements IDisposable   Public ReadOnly Property Container() As IContainer   Public MustOverride Property Site() As ISite   Protected ReadOnly Property DesignMode() As Boolean   Protected ReadOnly Property Events() As EventHandlerList End Class 

The Component base class gives a component direct access to both the container and the site. A component can also access the VS.NET designer host itself by requesting the IDesignerHost interface from the container:

 
 Dim designerHost As IDesignerHost = CType(Me.Container, IDesignerHost) 

In VS.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, We've built a sample. [3] Because components and controls share the same design-time features and because we like things that look snazzy, we built a digital/analog clock control with the following public members :

[3] While we use the term "we" to maintain consistency with the rest of the prose , it was actually Michael Weinhardt that built this sample as well as doing the initial research and even the initial draft of much of the material in this chapter. Thanks, Michael!

 
 Public Class ClockControl   Inherits Control   Public Sub New()   Public Property Alarm() As DateTime   Public Property IsItTimeForABreak() As Boolean   Public Event AlarmSounded   ... End Class 

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 VS.NET to break at the right moment.

[4] Remember that nonvisual components as well as controls are components for the purposes of design-time integration.

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 VS.NET itself, this means that you'll use one instance of VS.NET to debug another instance of VS.NET with a running instance of the component loaded. This may sound confusing, but it's remarkably easy to set up:

  1. Open the component solution to debug in one instance of VS.NET.

  2. Set a second instance of VS.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.

  3. Choose Set As StartUp Project on your component project.

  4. Set a breakpoint in the component.

  5. Use Debug Start (F5) to begin debugging.

At this point, a second instance of VS.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   Inherits Control   ...   Dim timer As Timer = New Timer()   ...   Public Sub New()       ...       ' Initialize timer       timer.Interval = 1000       AddHandler timer.Tick, AddressOf Me.timer_Tick       timer.Enabled = True   End Sub   ...   Sub timer_Tick(sender As Object, e As EventArgs)       ' Refresh clock face       Me.Invalidate()       ...   End Sub End Class 

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 :

 
 Sub timer_Tick(sender As Object, e As EventArgs)   ' Don't execute event if running in design time   If Me.DesignMode Then Exit Sub   Me.Invalidate()   ... End Sub 

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 16x16, 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:

 
 <ToolboxBitmap(GetType(ClockControlLibrary.ClockControl), _   "images.ClockControl.ico")> _     Public Class ClockControl   Inherits Control   ... End Class 

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 16x16, 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 propertiessuch as the Alarm property highlighted in Figure 9.10are 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:

 
 <Browsable(False)> _ Public Property IsItTimeForABreak() As Boolean   Get       ...   End Get   Set     ...   End Set End Property 
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.

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:

 
 <Category("Behavior"), Description("Alarm for late risers")> _ Public Property Alarm() As DateTime   Get      ...   End Get   Set      ...   End Set End Property 

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:

 
 <Category("Appearance"), Description("Whether digital time is shown"), _   DefaultValue(True)> _ Public Property ShowDigitalTime() As Boolean   Get      ...   End Get   Set      ...   End Set End Public 

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:

 
 <DefaultEvent("AlarmSounded")> _ Class ClockControl   Inherit Control   ... End Class 

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   Inherits Form   ...   Sub InitializeComponent()       ...       AddHandler Me.clockControl1.AlarmSounded, _           AddressOf Me.clockControl1_AlarmSounded       ...   End Sub   ...   Sub clockControl1_AlarmSounded(sender As Object, _       type As ClockControlLibrary.AlarmType)       ...   End Sub End Class 

You can also adorn your component with DefaultPropertyAttribute:

 
 <DefaultProperty("ShowDigitalTime")> _ Public Class ClockControl   Inherits Windows.Forms.Control   ... End Class 

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 End 

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:

 
 <DefaultValue(True), _   DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Hidden)> _ Public Property ShowDigitalTime() As Boolean   Get      ...   End Get   Set      ...   End Set End Property 

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:

 
 <Category("Behavior"), Description("Stuff to remember for later"), _   DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Content)> _ Public Property MessageToSelf() As MessageToSelfCollection   Get      ...   End Get   Set      ...   End Set End Property 

The generated InitializeComponent code for a single message looks like this:

 
 Sub InitializeComponent()   ...   Me.clockControl1.MessagesToSelf.AddRange( _       New ClockControlLibrary.MessageToSelf( _           New System.DateTime(2003, 2, 22, 21, 55, 0, 0), _             "Wake up")})   ... End Sub 

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:

 
 Dim _hostingForm As Form = CType(Me.Parent, 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:

 
 Dim _hostingForm As Form = Nothing <Browsable(False)> _ Public Property HostingForm() As Form   ' Used to populate InitializeComponent at design time   Get       If (hostingForm = Nothing And Me.DesignMode) Then           ' Access designer host and obtain reference to root component           Dim designer As IDesignerHost = _               CType(Me.GetService( _                 GetType(IDesignerHost)), IDesignerHost           If designer <> Nothing Then               hostingForm = _                 CType(designer.RootComponent, Form)           End If       End If       Return hostingForm   End Get   Set(ByVal Value As Form)       ...   End Set End Property 

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:

 
 Sub InitializeComponent()   ...   Me.myComponent1.HostingForm = Me   ... End Sub 

At run time, when InitializeComponent runs, it will return the hosting form to the component via the HostingForm property setter:

 
 Dim _hostingForm As Form = Nothing <Browsable(False)> _ Public Property HostingForm() As Form   Get       ...   End Get   ' Set by InitializeComponent at run time   Set(ByVal Value As Form)       If Not(Me.DesignMode) Then           ' Don't change hosting form at run time           If (Not _hostingForm Is Nothing) And _             (Not _hostingForm Is Value) Then               Throw New _                   InvalidOperationException( _                     "Can't set HostingForm at run time.")           End If       Else           hostingForm = Value       End If   End Set End Property 

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:

 
 Dim _primaryAlarm As DateTime = DateTime.Now Dim _backupAlarm As DateTime = DateTime.Now.AddMinutes(10) 

Both properties should check to ensure that the values are valid:

 
 Public Property PrimaryAlarm() As DateTime   Get       Return _primaryAlarm   End Get   Set(ByVal Value As DateTime)       If Value >= backupAlarm Then _           Throw New ArgumentOutOfRangeException( _             "Primary alarm must be before Backup alarm")       _primaryAlarm = Value   End Set End Property Public Property BackupAlarm() As DateTime   Get       Return _backupAlarm   End Get   Set(ByVal Value As DateTime)       If Value < primaryAlarm Then _           Throw New ArgumentOutOfRangeException( _             "Backup alarm must be after Primary alarm")       _backupAlarm = Value   End Set End Property 

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 :

 
 Sub InitializeComponent()   ...   ' clockControl1   Me.clockControl1.BackupAlarm = New System.DateTime(2003, 11, _     24, 13, 42, 47, 46)   ...   Me.clockControl1.PrimaryAlarm = New System.DateTime(2003, 11, _     24, 13, 57, 47, 46)   ... End Sub 

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 Sub BeginInit()   Public Sub EndInit() End Interface 

When a component implements this interface, calls to BeginInit and EndInit are serialized to InitializeComponent:

 
 Sub InitializeComponent()   ...   CType(Me.clockControl1, ISupportInitialize).BeginInit()   ...   ' clockControl1   Me.clockControl1.BackupAlarm = New System.DateTime(2003, 11, _     24, 13, 42, 47. 46)   ...   Me.clockControl1.PrimaryAlarm = New System.DateTime(2003, 11, _     24, 13, 57, 47, 46)   ...   CType(Me.clockControl1, ISupportInitialize).EndInit()   ... End Sub 

The call to BeginInit signals the entry into initialization batch mode, a signal that is useful for turning off value checking:

 
 Public Class ClockControl   Inherits Control   Implements ISupportInitialize   ...   Dim initializing As Boolean = False   ...   Sub BeginInit() Implements ISupportInitialize.BeginInit       initializing = True   End Sub   ...   Public Property PrimaryAlarm() As DateTime       Get           ...       End Get       Set(ByVale Value As DateTime)           If Not(initializing) Then               ' check value           End If           primaryAlarm = Value       End Set   End Property   Public Property BackupAlarm() As DateTime       Get           ...       End Get       Set(ByVal Value As DateTime)           If Not(initializing) Then               ' check value           End If           backupAlarm = Value       End Set   End Property End Class 

Placing the appropriate logic into EndInit performs batch validation:

 
 Public Class ClockControl   Inherits Control   Implements ISupportInitialize   Sub EndInit() Implements ISupportInitialize.EndInit       If primaryAlarm >= backupAlarm Then _           Throw New ArgumentOutOfRangeException( _             "Primary alarm must be before Backup alarm")   End Sub   ... End Class 

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   Inherits Control   Implements ISupportInitialize   ...   Sub EndInit() Implements ISupportInitialize.EndInit       ...       If Not(Me.DesignMode) Then           ' Initialize timer           timer.Interval = 1000           AddHandler timer.Tick, AddressOf Me.timer_Tick           timer.Enabled = True       End If   End Sub End Class 

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.



Windows Forms Programming in Visual Basic .NET
Windows Forms Programming in Visual Basic .NET
ISBN: 0321125193
EAN: 2147483647
Year: 2003
Pages: 139

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net