Design-Time Support for Custom Controls

Overview

The Custom Controls you have explored so far are full of promise. Being able to drop a tool like a directory browser or thumbnail viewer directly into your application without writing a single line of extra code is a remarkable advantage. However, there is one caveat. Though your custom controls perform wonders at runtime, many of them act oddly while a form is being designed. By default, only user controls appear in the toolbox, and even they only appear with a generic icon. Inherited controls need to be created in code, unless you create a similar control and manually edit the hidden designer instructions. Even if you use this trick, the results aren't always what you expect.

This chapter sorts through these quirks and shows how to create a control that behaves properly at design time. Some of the topics this chapter tackles include:

This chapter will explore these topics, and use them to revise some of the examples introduced in the previous chapter.

 
Chapter 8 - Design-Time Support for Custom Controls
 
byMatthew MacDonald  
Apress 2002
Companion Web Site
 

Control Designer Basics

Adding a basic level of design-time support to your controls is easy. In this section you learn how you can outfit your control with a custom toolbox icon, resource files, and support for the Properties window.

Attributes

Designer attributes are the first line in custom control support. They instruct the IDE how to treat various parts of your control. Attributes are a unique development in .NET programming. To specify this type of information about a custom control in another programming language, you would either need to create a separate file in a proprietary format (and learn a new syntax), or use some sort of visual tool. With attributes, the information that describes your control can be easily created and edited alongside your code, but it is still cleanly separated from the logic that generates the user interface.

The previous chapter developed a Progress user control that displayed a synchronized label paired with a progress bar. To make it work, three properties were added: Value, Step, and Maximum. You may have noticed that these properties appear in the design window grouped under the generic "Misc" category without any additional information (see Figure 8-1).

Figure 8-1: Nondescript properties

You can improve on this situation using attributes. The example below adds a Description, Category, and DefaultValue attribute to the Value property. Note that when you use more than one attribute, they are all enclosed between angled brackets, and separated with commas. The underscore character is used to spread the attributes over several lines for better readability.

[Description("The current value (between 0 and Maximum) which sets " + "the position of the progress bar"), Category("Behavior"), DefaultValue(0)] public int Value { get { return Bar.Value; } set { Bar.Value = value; UpdateLabel(); } }

The result of applying these attributes is shown in Figure 8-2.

Figure 8-2: A property configured with attributes

All these attributes are found in the System.ComponentModel namespace, along with many more that allow you to configure aspects of your control's designtime behavior. Table 8-1 lists the most useful attributes you can use to configure properties.

Table 8-1: Basic Control Property Attributes

Attribute

Description


AmbientValue(true|false)

Indicates that the value for a property is derived from the control's parent. For example, most controls have an ambient Font and BackColor property—if these values are not set, the Font and BackColor of the parent is used automatically.


Browsable(true|false)

If false, indicates that a property should not be shown in the Properties window. However, the property is still accessible through code.


Category("")

Sets the category under which the property appears in the Properties window. If a category with this name doesn't exist, it is created.


DefaultValue()

Sets the initial value that will be used for this property when the control is created.


Description("")

Specifies the text description that will be displayed for this property in the Object Browser of Properties window.


DesignOnly(true|false)

When set to true, this property is only available at design time. This is typically used with special properties that configure how a control behaves at design time (like a SuppressUI property), and don't correspond to a "real" piece of information about the control.


ImmutableObject(true|false)

When set to true on an object property, this attribute ensures that the subproperties of this object is displayed as read-only. For example, if you apply this to a property that uses a Point object, the X and Y subproperty will be read-only.


Localizable(true|false)

When set to true, the design-time value for this property is stored in a resource file instead of in the generated code. This makes it easy to swap the value later by introducing a new resource file. When the user configures properties that don't use this attribute, the appropriate code is inserted in the hidden designer region of the form, unless it requires a special data type (like an image) that must be stored in a resource file.


MergableProperty(true|false)

Configures how the Properties window behaves when more than one instance of this control is selected at once. If false, the property is not shown. If true (the default), the property can be set for all selected controls at once.


NotifyParentProperty(true|false)

Set this to true to indicate that a parent property should receive notification about changes to the property's value (and update its display accordingly). For example, the Size property has two nested properties: Height and Width. These nested properties should be marked with this attribute.


ParenthesizePropertyName(true|false)

When true, indicates that the property should be displayed with brackets around it in the Properties window (like the Name property).


ReadOnly(true|false)

When true, this property is read-only in the Properties window at design time.


RefreshProperties()

You use this attribute with a value from the RefreshProperties enumeration. It specifies whether the rest of the Properties window must be updated when this property is changed (for example, if one property procedure could change another property).


A few attributes can be applied to your custom control class declaration, rather than a specific property. These include two attributes that set the default event and property. Here's how you could use these attributes with the DirectoryTree developed in the previous chapter:

[DefaultEvent("DirectorySelected"), DefaultProperty("Drive")] public class DirectoryTree : TreeView

Table 8-2 lists the useful designer attributes that you can apply to the class definition.

Table 8-2: Basic Control Class Attributes

Attribute

Description


DefaultEvent

When the application programmer double-clicks your control, Visual Studio .NET automatically adds an event handler for the default event.


DefaultyProperty

The DefaultProperty is the property that is highlighted in the Properties window by default, the first time the control is selected.


You can also use some advanced attributes to support licensing and custom windows for property settings. You learn about these topics a little later in this chapter.

Basic Serialization

When creating a property, you can add additional methods to configure its default value, and specify whether changes should be serialized to the Windows designer code in the form. You add this extra logic by creating two optional methods for each property: ShouldSerializePropertyName() and ResetPropertyName().

For example, if you have a property named Drive, you could add the following methods:

public void ResetDrive() { // (Reset code goes here.) } public bool ShouldSerializeDrive() { // (Determine if serialization is needed here.) }

Visual Studio .NET automatically invokes the ResetDrive() method when the control is first created (to set a default value), or whenever the user right-clicks the property in the Properties window and chooses Reset. If you don't use the ResetDrive() method, Visual Studio .NET uses whatever value is specified in the DefaultValue attribute applied to the Drive property. Thus, you should use either a DefaultValue attribute or the custom ResetDrive() method, not both.

One reason that you might want to use the custom ResetDrive() method instead of a DefaultValue attribute is so that you can make a runtime decision about what value to use. For example, you could set this property in accordance with other control properties, or by examining the hard drive to determine what drive letters are valid.

The ShouldSerializeDrive() method performs a slightly different task. It returns true or false to indicate whether the current value of the Drive property should be serialized to the form's designer code. If you don't include this method, Visual Studio .NET always generates the designer code in response to the values chosen by the user at design time. One reason that the ShouldSerializePropertyName() method is often used is to avoid serializing information when this information corresponds to the default value. This results in more economical designer code.

Here's a complete example for the Drive property:

private Char drive; public Drive { get { return drive; } set { drive = value; RefreshDisplay(); } } public void ResetDrive() { drive = "C"; } public bool ShouldSerializeDrive() { // Serialize the change as long as it does not equal the default value. return !(drive == "C"); }

The Toolbox Bitmap

Adding a toolbox icon is refreshingly easy. All you need to do is add a bitmap to your project and ensure it has the same file name as your custom control class. This bitmap must meet some basic criteria:

  • It must be 16 pixels by 16 pixels. Otherwise, Visual Studio .NET attempts to scale it and the results will be ugly.
  • It must use only 16 colors.

Once you add the file, use the Properties window to set the Build Action for it to Embedded Resource. Then, recompile the control project. Figure 8-3 shows an example: the DirectoryTree control project with the required image file.

Figure 8-3: Configuring a toolbox bitmap

When you add the control to a client project, the embedded bitmap appears in the toolbox, as shown in Figure 8-4.

Figure 8-4: A custom toolbox bitmap

Resource Files

The previous chapter developed an extender provider that displayed a custom Help icon next to ordinary .NET controls. One of the flaws in the design was that the icon was read from a file. This means it has to be in the current directory of the program that is using the HelpIconProvider.

A better approach is to embed the binary data for the icon as a resource in the compiled DLL. In .NET, this is accomplished using .resx files. Visual Studio .NET creates a .resx file automatically for every form, and uses it to store images (or other binary data) that you set at design time.

To see the .resx files in your project, select Project → Show All Files from the Visual Studio .NET menu. A .resx file appears under each form node in the Solution Explorer, as shown in Figure 8-5.

Figure 8-5: A resource file

If you opened a .resx file in Notepad or a similar text editor, you would discover that it is actually an XML file that contains a name/value pair for each resource. For example, the snippet that follows defines the binary data for a picture in a PictureBox control.

R0lGODlhEAAQAPcAAP7+/f/99fLy8+7z/O7x9ubt++jt9uPp8+Dm8///7vLx7Ozt7uPj5Mbz/9vq/9rj 9Nfh8s7p/93i7dTe8cre/8rY89Hc783U4MfQ48bP4dzd39LS0sDJ28rKysbIysLCwqjd/qnU/73J4JjP /JHJ/7vG3brBz6G555Gt6Iev7LG7yqi1z6Kxz5qv2ZCo2J+v1Zar1JWkwoGaz7u7u7Ozs6qssaysrJid ppOZpZqamnGy/3mk9muY9Xuc4Hea4kyL/3CS2X+c12mO2GaM2myKyWqGwl2H3n2BiVJ/10Bx0Fl+x1F5 xz1z4DZx5itn4jNr2zls0DRp0zpszTRozjJmzTNjxSxizCpfyiNbyytfxSRZwCBWxBVU0RpVyR1VxRFM wz1luSVXuRFIuRZNtw5JvQ1GugExvX19fXJycm9zeWxsbGNjY1lZWlJSUk5OTgAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP// /yH5BAEAACQALAAAAAAQABAAAAj8AEkIBPBggoWDEyQIEMiQxIAJBwBIlIhggoaGDyU2ADFiRAiJEzwM nAAggo4UEhQg2PGDAoAJHUg8IODgh5EEAE4ACMCkyQAENxiQ5OGEyAEkZoIAGMLFBwAONiAUeBLlCpYu XzAASIJFCgALah5UwEKFipUtKwD08GIlC4EJbQp6KYulCAAUX6y0FQDBDQQAVbBMwULkARSyXpYAyNCG pAsyeqUIuWLlyhgRBnCsWfBXSZkvMQAA+VJGBgAIbHIAuHAggIwxYFpoCfMiwIM0aC4KMPEAAAMWMEoo 4HtEzQyGDGpggPCgOQQVbNDQaEhiwQc1bdxob3NmA8OAADs=

You can also view the .resx file in Visual Studio .NET by double-clicking it. It displays the list of pairs in a special dataset view (see Figure 8-6).

Figure 8-6: A .resx file in Visual Studio .NET

When a project is compiled, the .resx is converted to a binary .resources file. This file is then embedded in the compiled assembly (DLL or executable file) for the application. That guarantees that the required information is always available, without needing to rely on an external file that could be moved or deleted.

It is possible to create .resx files programmatically by hand, but the process is labor intensive. With custom control development, you can take a simple shortcut that reaps the same benefits. The process works like this:

  1. Add a new form to your control project. This form will not be shown; its purpose is to store resources that your control will use. For the HelpIconProvider, you might create a form called HelpIconResources.
  2. Create picture boxes for the images you need to access and load the appropriate image into each picture box at design time. Behind the scenes, Visual Studio .NET creates a .resx file for the form and adds the binary data for each picture.
  3. Read through the form's designer region to find the code that reads the images from the resource file.

    System.Resources.ResourceManager resources; resources = new System.Resources.ResourceManager(typeof(HelpIconResources)); this.PictureBox1 = new System.Windows.Forms.PictureBox(); this.PictureBox1.Image = (System.Drawing.Bitmap) resources.GetObject("PictureBox1.Image");

  4. Use this code to read the appropriate resource in your control class. For example, consider the original HelpIconProvider code:

    PictureBox pic = new PictureBox(); pic.Image = Image.FromFile("Help.gif");

    This can be replaced with the following code that retrieves the picture from the resource file:

    PictureBox pic = new PictureBox(); System.Resources.ResourceManager resources; resources = new System.Resources.ResourceManager(typeof(HelpIconResources)); pic.Image = (System.Drawing.Bitmap)Resources.GetObject("PictureBox1.Image");

It's important to realize that every code file in Visual Studio .NET has an associated .resx file, even if does not correspond to a form. For example, you can add resources directly to a HelpIconProvider.resx file by adding picture boxes to the design portion of the HelpIconProvider.vb file (see Figure 8-7).

Figure 8-7: Adding resources to an ordinary code file

The fact that HelpIconProvider.vb doesn't have a graphical display doesn't stop you from adding resources. In fact, the appropriate designer code is added directly to a collapsed "Component Designer generated code" region in your control class! The approach you take is up to you. You can refer to the ExtenderProvider project, which is included with the online samples for Chapter 7, to see one example of how a resource file can be embedded in a custom control. As you become more comfortable with the .resx format, you may even want to create your .resx files by hand. Refer to the MSDN documentation for the System.Resources namespace for more information.

 
Chapter 8 - Design-Time Support for Custom Controls
 
byMatthew MacDonald  
Apress 2002
Companion Web Site
 

Testing Custom Controls

Testing custom controls can sometimes be a little awkward. Visual Studio .NET provides several options:

  • You can add test forms directly to your control class projects and remove them when they are no longer needed (or just set the Build Action to None instead of Compile, so that they are retained but not added to the compiled DLL).
  • You can create a solution with two projects: one that tests the control, and one that uses the control. However, you may find that you need to close the solution and reopen it to see changes to the control's design-time behavior.
  • You can open two instances of Visual Studio .NET, one with the control project, and one with the control test project. Once again, you may need to close and reopen the client project to see changes in a control's design-time behavior.
  • You can use the specialized PropertyGrid control examined in this section.

Even if you don't follow one of these specialized approaches, if an error occurs in your custom control code and the .vb source file is available, Visual Studio .NET automatically loads the file and enters break mode, pausing execution on the offending line.

Debugging Design Time Support

If you test your custom control in a project with the control source code, you are able to set breakpoints and use other debugging tricks. However, these breakpoints are ignored at design time, and only have an effect while the control is running "live."

This limitation can hamper your testing to a certain extent. Developing good design-time support for your control requires a different set of considerations than creating its basic functionality. One solution to this problem is to debug the IDE itself. To accomplish this, add your control to the toolbox, and then configure your custom control project to start Visual Studio .NET when you run it (see Figure 8-8). You can now set breakpoints in your control code or custom designer code that are triggered as the control is manipulated in the IDE.

Figure 8-8: Debugging design-time support

Testing for Design Mode

You may also notice some quirky design-time behavior that doesn't occur if the control is added programmatically. For example, if you add the DirectoryBrowser at design time, you'll discover some quirky behavior.

At first, it seems straightforward enough-once you set the Drive property, the corresponding directory tree appears. You can even expand nodes and browse the directory structure at design time, as shown in Figure 8-9.

Figure 8-9: The DirectoryTree at design time

However, when you start your program, a second set of identical directory nodes appears. The problem is that the nodes you create at design time are automatically serialized to the form's designer code. At runtime, the control is re-created, the directory nodes are rebuilt when the drive property is set, and then the serialized nodes are added.

There are several ways you can resolve this problem. First, you could change the order of the form designer lines so that the Drive property is configured after the serialized nodes are added (setting the Drive property automatically clears the current list of nodes). Alternatively, you could create a custom designer, as you do later in this chapter. The simplest approach, however, is just to configure DirectoryTree control so that it doesn't provide its directory Node display at design-time. You can do this by explicitly checking what mode the control is in before refreshing the display.

public char Drive { get { return drive; } set { drive = value; if (!this.DesignMode) { RefreshDisplay(); } } }

The PropertyGrid Control

Another way to test your control is using .NET's specialized PropertyGrid control. This control is actually an exact replica of the Properties window contained in the Visual Studio .NET environment. You can add this control to a form, and use it to run your custom control through its paces by modifying any of its properties. This is a good practice to get into-if you try to set invalid property values, you'll probably discover that your control isn't as successful as you expect at rejecting them. By default, the PropertyGrid control doesn't appear in the toolbox. To add it, you need to choose Customize Toolbox and find it in the list. The PropertyGrid provides properties that allow you to format its appearance and configure its display. The most important property, however, is SelectedObject. When you set the SelectedObject to an instance of a control, the grid automatically fills with a list of all the available properties. When you change a property in the grid, it is applied to the control immediately. Figure 8-10 shows a test project that combines an instance of the DirectoryTree control with a PropertyGrid. This example is included with online samples for this chapter as the project named DirectoryTreeClient.

Figure 8-10: The PropertyGrid control

  Tip 

Interestingly, you can use the PropertyGrid control with any object, regardless of whether it is a control, component, or simple class. The PropertyGrid allows you to modify any public property exposed by the class.

 
Chapter 8 - Design-Time Support for Custom Controls
 
byMatthew MacDonald  
Apress 2002
Companion Web Site
 

Custom Designers

One of the problems with traditional ActiveX control development is that details about a control's design-time behavior are mingled with details about its runtime behavior. With .NET control development, this problem is neatly sidestepped by a new feature called a control designer.

A control designer provides the design-time behavior for a control. The .NET framework provides a basic control designer in the System.Windows.Forms. Design.ControlDesigner class, and some derived classes that add support for child control containment and scrolling. Figure 8-11 shows the hierarchy.

Figure 8-11: Control designer classes

Controls can also derive their own custom designers. Why would you create your own designer?

  • To add special designer tools, like context menu options.
  • To remove inappropriate events or properties from view (or add designtime only events or properties).
  • To add support for controls that contain other controls (like the toolbar) or controls with special needs (like menus).

The next few sections consider all these topics by designing and enhancing a DirectoryTreeDesigner class that works with the DirectoryTree control.

Filtering Properties and Events

Sometimes, an event or property needs to be hidden from a control, but not removed entirely. For example, the ProgressBar control provides a Text property, which it inherits from the base Control class. This property can be used at the programmer's discretion, but it does not have any visible text because the ProgressBar doesn't provide a caption. For this reason, the Text property should be hidden from the Properties window.

If you are defining or overriding a property, you can use the Browsable attribute to keep it from appearing in the Properties window. However, consider the TreeView control, which provides a Nodes collection. You may have noticed that the DirectoryTree displays the Nodes property in the designer, and allows it to be modified, even though the display is built automatically at runtime based on the Drive property. The TreeView.Nodes property is not overridable, so you can't use the Browsable attribute. However, you can create a custom designer that ensures it won't appear at design time.

Designers provide six methods from the IDesignerFilter interface that you can override to filter properties, events, and attributes. These methods are listed in Table 8-3.

Table 8-3: ControlDesigner Filtering Methods

Method

Description


PostFilterAttributes

Overrides this method to remove unused or inappropriate attributes.


PostFilterEvents

Overrides this method to remove unused or inappropriate events.


PostFilterProperties

Overrides this method to remove unused or inappropriate properties.


PreFilterAttributes

Overrides this method to add attributes.


PreFilterEvents

Overrides this method to add events.


PreFilterProperties

Overrides this method to add properties.


To use filtering with the DirectoryTree, create a custom designer class that derives from ControlDesigner. In this designer, you can override the PostFilterProperties() method, and use the provided properties collection to remove properties that you don't want displayed. You remove them by name.

public class DirectoryTreeDesigner : ControlDesigner { protected override void PostFilterProperties(System.Collections.IDictionary properties) { properties.Remove("Nodes"); } }

The next step is to link the custom designer to the DirectoryTree control. To do this, you use the Designer attribute, and specify the appropriate designer type.

[Designer(typeof(DirectoryTreeDesigner))] public class DirectoryTree : TreeView

Now, when you recompile the control and test it in the client, you'll notice that the Nodes property does not appear in the Properties window. However, the Nodes property is still accessible in code. This allows clients to perform other useful tasks (like enumerating through the collection of nodes) at their discretion. This code also ensures that the Nodes collection is not serialized at design time, effectively sidestepping the problem where the same set of drive nodes are added more than once to an instance of the DirectoryTree control.

Designer Verbs

You can also use a custom designer to add to the context menu that is displayed when a programmer right-clicks your control in the design environment. This menu contains some standard options provided by Visual Studio .NET, but it can also contain your commands (technically known as verbs).

To add verbs, you need to override the Verbs property in your custom designer, create a new DesignerVerbCollection, and add the appropriate DesignerVerb object entries. Your control designer handles the verb click event, generally by updating the associated control.

The following example retrieves a list of all the drives on the current computer, and adds a context menu entry for each one. The user can click the appropriate entry to set the Drive property of the control.

public class DirectoryTreeDesigner : ControlDesigner { private DesignerVerbCollection verbs = new DesignerVerbCollection(); public DirectoryTreeDesigner() { // Configure the designer verb collection. string[] drives = System.IO.Directory.GetLogicalDrives(); foreach (string drive in drives) { verbs.Add(new DesignerVerb("Set Drive " + drive, new EventHandler(OnVerb))); } } public override DesignerVerbCollection Verbs { get { return verbs; } } protected void OnVerb(object sender, EventArgs e) { // Retrieve the selected drive. char driveLetter = ((DesignerVerb)sender).Text[10]; // Adjust the associated control. ((DirectoryTree)this.Control).Drive = driveLetter; } }

The resulting context menu for the DirectoryTree control is shown in Figure 8-12.

Figure 8-12: Designer verbs

Generally, you won't use your designer verbs to provide settings for a simple property. A more interesting technique is to provide higher-level configuration operations that adjust several properties at once. One example of this is found in the ASP.NET Calendar control, which allows the user to choose a theme from a list of preset choices (see Figure 8-13). When a theme is selected, several properties are modified in conjunction.

Figure 8-13: The Calendar themes

Implementing this design is refreshingly easy. Just add a Windows form to your project and display it when the appropriate designer verb is selected. Here's another simple example using the DirectoryTree. This time, only a single verb is available, which then displays a window that allows the user to choose a drive. When a drive is chosen, a public form-level variable is set and retrieved by the designer, which applies the change. This approach is more manageable than the previous design, and doesn't clutter the context menu with drive letters.

public class DirectoryTreeDesigner : ControlDesigner { private DesignerVerbCollection verbs = new DesignerVerbCollection(); public DirectoryTreeDesigner() { verbs.Add(new DesignerVerb("Set Drive", new EventHandler(OnVerb))); } public override DesignerVerbCollection Verbs { get { return verbs; } } protected void OnVerb(object sender, EventArgs e) { // Show the form. SelectDrive frm = new SelectDrive(); frm.DriveSelection = ((DirectoryTree)this.Control).Drive; frm.ShowDialog(); // Adjust the associated control. ((DirectoryTree)this.Control).Drive = frm.DriveSelection; } }

The SelectDrive form is quite simple:

public class SelectDrive : System.Windows.Forms.Form { public char DriveSelection; // (Designer code omitted.) private void SelectDrive_Load(object sender, System.EventArgs e) { string[] drives = System.IO.Directory.GetLogicalDrives(); lstDrives.DataSource = drives; // Select the current drive. lstDrives.SelectedIndex = lstDrives.FindString( DriveSelection.ToString()); // Attach the event handler. // This step is performed after the selected index is set, // to prevent it from being overwritten as the list is built. lstDrives.SelectedIndexChanged += new EventHandler(lstDrives_SelectedIndexChanged); } private void lstDrives_SelectedIndexChanged(object sender, System.EventArgs e) { DriveSelection = lstDrives.Text[0]; } }

Figure 8-14 shows the drive selection window that appears when the user edits the Drive property.

Figure 8-14: A custom drive selection window

One quirk remains in the control designer. When the DirectoryTree.Drive property is modified by the designer, the Properties window is not updated until the control is deselected and then reselected. To correct this defect, you need to explicitly notify the IDE that a change has been made.

The rewritten OnVerb() method handles this detail:

protected sub OnVerb(object sender, EventArgs e) { // Show the form. SelectDrive frm = new SelectDrive(); frm.DriveSelection = ((DirectoryTree)this.Control).Drive; frm.ShowDialog(); // Adjust the associated control. ((DirectoryTree)this.Control).Drive = frm.DriveSelection; // Notify the IDE that the Drive property has changed. PropertyDescriptorCollection properties; properties = TypeDescriptor.GetProperties(typeof(DirectoryTree)); PropertyDescriptor changedProperty = properties.Find("Drive", false); this.RaiseComponentChanged(changedProperty, "", frm.DriveSelection); }

The final designer code for this example can be found in the DirectoryTree project with the online samples for this chapter.

  Note 

When you add a form to a control project in this way, the client is able to see the form class in your designer and create and display instances of it. If this isn't the behavior you want, you need to nest your form class inside your control class and make it private or protected. Unfortunately, if you do this you have to forego Visual Studio .NET's design-time support for the form and manually copy the form code into the class.

Control Designer Notifications

Visual Studio .NET only creates one instance of a control designer per form. For example, if you create a custom DirectoryTreeDesigner class and add three DirectoryTree controls to a form, the single DirectoryTreeDesigner instance is reused to provide the behavior for all three trees. This detail can usually be ignored, unless you are designing controls that contain other special controls. For example, the TabControl class is designed to host one or more TabPage controls. Every time a TabPage is added, the TabControl needs to update its visual appearance (for example, the tab strip at the top) accordingly.

To perform this sort of functionality, you need to create a control class that derives from ControlDesignerParent, and then access the features of the IComponentChangeService. Luckily, the ControlDesigner class provides a GetService() method to help you out. Here's an example that uses the GetService() method to register for notifications when child components have been added:

public void MyControlDesigner() { IComponentChangeService service; service = GetService(typeof(IComponentChangeService)); service.ComponentAdded += new ComponentEventHandler(ComponentAdded); }

You should place this constructor inside your custom designer class, so that the designer registers for child control notifications as soon as it is created.

This book doesn't consider custom container controls, and so none of the examples use the IComponentChangeService. However, if it's something you would like to explore, start with the overview of key events in Table 8-4.

Table 8-4: IComponentChangeService Methods

Event

Description


ComponentAdded

Triggered when a component is added to the control at design time.


ComponentAdding

Triggered when a component is in the process of being added to the control at design time.


ComponentChanged

Triggered when a contained component has changed at design time.


ComponentChanging

Triggered when a component is in the process of changing at design time.


ComponentRemoved

Triggered when a component is removed at design time.


ComponentRemoving

Triggered when a component is in the process of being removed.


 
Chapter 8 - Design-Time Support for Custom Controls
 
byMatthew MacDonald  
Apress 2002
Companion Web Site
 

Data Types and UITypeEditors

The Properties window does a solid job of providing support for all common data types. Basic data types like strings and numbers are all seamlessly supported. If you create a property that uses a custom enumeration, the Properties window automatically provides a drop-down list with the values from the enumeration.

For example, consider the DisplayStyle property shown here:

public enum Display { Standard, SpecialDirectoryPictures, AllDirectoryPictures } private Display displayStyle; public Display DisplayStyle { get { return displayStyle; } set { displayStyle = value; } }

The enumerated values are shown in the Properties window (see Figure 8-15).

Figure 8-15: Enumerations in the Properties window

  Note 

Remember, even if you use an enumerated value, you still need to perform some error-checking in your property procedure. Though programmers won't be able to submit an invalid value through the Properties window, nothing prevents them from using code to directly set an integer value that doesn't correspond to a valid value in the enumeration.

In addition to this basic level of support, you may have noticed that some richer object types have additional design-time support. For example, if you create a Property that has a Font or Color data type, a special color picker or font selection dialog is used in the Properties window. Similar magic happens if you create a Collection property. This support is provided through .NET UITypeEditor classes, which are special components that provide the design-time user interface that allows the programmer to set complex properties.

Using Prebuilt UITypeEditors

The base UITypeEditor class is found in the System.Drawing.Design namespace. You can inherit from this class to create your custom UITypeEditors, or you can use one of the derived classes that are provided with the .NET framework. These classes are listed in Table 8-5 (UITypeEditors that are only usable with specific web controls have been omitted).

Table 8-5: UITypeEditors in the .NET Framework

Class

Description


System.ComponentModel.Design.ArrayEditor

Edits an array by allowing the programmer to enter a list of strings. Used automatically for supported arrays.


System.Drawing.Design.FontEditor

Allows the programmer to select and configure a font. Used automatically for font properties.


System.Drawing.Design.ImageEditor

Allows the programmer to create an Image object by selecting a bitmap from an open file dialog. Used automatically for image types.


System.Web.UI.Design.WebControls.RegexTypeEditor

Allows the programmer to choose a regular expression from a list of common choices. This UITypeEditor works with string properties.


System.Windows.Forms.Design.AnchorEditor

Allows the Anchor property to be set at design time.


System.Windows.Forms.Design.FileNameEditor

Allows a fully qualified filename to be set by choosing a file from an open file dialog box. This UITypeEditor works with string properties.


Depending on the data type of your property, .NET may use the corresponding UITypeEditor automatically (for example, with a Font). On the other hand, some UITypeEditors do not have dedicated types. An example is the RegExTypeEditor, which allows the programmer to choose a common regular expression for a control property. The regular expression is stored as an ordinary string, so it needs to be explicitly associated with the appropriate UITypeEditor.

You associate a property with a UITypeEditor using the Editor attribute. Consider this example:

private string regEx = ""; [Editor(typeof(System.Web.UI.Design.WebControls.RegexTypeEditor), typeof(UITypeEditor))] public string ValidationExpression { get { return regEx; } set { regEx = value; } }

When the programmer clicks this property in the Properties window, an ellipsis (…)appears next to the property name. If the programmer clicks the ellipsis button, a full dialog appears with common regular expression choices (see Figure 8-16).

Figure 8-16: The RegexTypeEditor

  Note 

Interestingly, this type editor is originally designed for the validation controls provided with ASP.NET, and is provided alongside the web controls in the .NET namespaces. However, it works equally well with a Windows control.

Custom UITypeEditors

You can also develop custom UITypeEditor classes to allow special settings to be configured. For example, consider the TreeView control. Its Nodes property is a collection, but it doesn't use the standard collection editor (which only allows strings to be entered). Instead, it uses its specialized UITypeEditor.

To create a custom type editor, you must first create a class that derives from System.Drawing.Design.UITypeEditor. You can then override the four methods shown in Table 8-6.

Table 8-6: UITypeEditor Overridable Methods

ClassMethod

Description


EditValue()

Invoked when the property is edited. Generally, this is where you would create a special dialog box for property editing.


GetEditStyle()

Specifies whether the type editor is a DropDown (provides a list of specially drawn choices), Modal (provides a dialog box for property selection), or None (no editing supported).


GetPaintValueSupported()

Use this to return true if you are providing a PaintValue() implementation.


PaintValue()

Invoked to paint a graphical thumbnail that represents the value in the property grid.


The PaintValue() supported technique requires a little GDI+ wizardry, and you consider an example that uses it in Chapter 13. The next example, however, uses the EditValue() method with the DirectoryTree control. It allows editing of the Drive property by presenting the dialog box developed earlier.

public class DriveEditor : UITypeEditor { public override System.Drawing.Design.UITypeEditorEditStyle GetEditStyle( System.ComponentModel.ITypeDescriptorContext context) { // We will use a window for property editing. return UITypeEditorEditStyle.Modal; } public override object EditValue( System.ComponentModel.ITypeDescriptorContext context, System.IServiceProvider provider, object value) { SelectDrive frm = new SelectDrive(); // Set current drive in window. frm.DriveSelection = (char)value; frm.ShowDialog(); // Return the new value. return frm.DriveSelection; } public override bool GetPaintValueSupported( System.ComponentModel.ITypeDescriptorContext context) { // No special thumbnail will be shown for the grid. return false; } }

The type editor is attached to the appropriate property using an Editor attribute:

[Editor(typeof(DriveEditor), typeof(UITypeEditor))] public Char Drive

One benefit to this design is that you can reuse this UITypeEditor with any drive property in any control. It's specific to the property data type, not the control.

An alternative approach is to use a DirectoryInfo object to represent the drive instead of an underlying char. Because the property editing is now handled by the UITypeEditor, there's no need to choose a basic type that can be edited with the default design-time support built into the property grid. That would also give you the freedom to enhance the control so it could be set to initially display a specific subdirectory, for example.

 
Chapter 8 - Design-Time Support for Custom Controls
 
byMatthew MacDonald  
Apress 2002
Companion Web Site
 

Licensing Custom Controls

Licensing in the .NET world is far more customizable and far less painful than it was with ActiveX controls. The .NET framework provides several licensing classes in the System.ComponentModel namespace. By using and extending these classes, you can grant or allow access to your control, using ordinary .NET code to check external resources like the Windows registry, an XML file, or even a remote web service for registration information.

Simple LIC File Licensing

To best understand .NET licensing, it helps to start with a simple example using the LicFileLicenseProvider class. This class doesn't provide any real protection, but it's a stepping-stone to the more advanced licensing strategies you look at next.

The LicFileLicenseProvider searches for a text file in the same directory as the control assembly. This LIC file uses the control's fully qualified class name for a filename, so the DirectoryTree control requires a license file named DirectoryTreeControl.DirectoryTree.LIC. Inside this file is a simple predefined text string in the format "[Component] is a licensed component."

Thus, the contents of the DirectoryTreeControl.DirectoryTree.LIC file would be:

DirectoryTreeControl.DirectoryTree is a licensed component.

This file must be placed in the client project's bin directory (where Visual Studio .NET compiles the final exe just prior to launching it).

  Note 

It's worth noting that these LIC files don't need to be distributed with a client application.When you compile a Windows program, a license.licx file is created with all the licensing information for all license controls. This file is compiled as a binary resource and embedded in the final client assembly. However, if another developer wants to create a client application with your control, a LIC source file is needed.

To enforce LIC file licensing, you need to add a LicenseProvider attribute to your control class that tells .NET to use the LicFileProvider class to validate licenses.

[LicenseProvider(typeof(LicFileLicenseProvider))] public class DirectoryTree : TreeView

Additionally, you need to create the license when the control is created, using the static Validate() method of the LicenseManager Help class:

private License license; public DirectoryTree() { license = LicenseManager.Validate(this.GetType(), this); }

The Validate() method throws a LicenseException if it doesn't find the correct string in the LIC file, and refuses to create your control. This restriction applies both to design time and runtime control creation.

Finally, you need to dispose of the license when the control is disposed.

protected override void Dispose(bool disposing) { if (license != null) { license.Dispose(); } base.Dispose(disposing); }

Custom LIC File Licensing

Clearly, simple LIC file licensing doesn't offer much in the way of protection. Any user who knows a little about the .NET framework will realize the generic format that must be created for a LIC file. However, you can add more stringent requirements by creating a custom license provider based on the LicFileLicenseProvider.

All you need to do is inherit from the class and override the IsValid() method. The IsValid() method receives the contents of the LIC file, and returns true or false to indicate if the contents are correct. Thus, you could use the IsValid() method to check a license number against a company-specific algorithm.

The example below extracts the first three characters from the license file, and verifies that they correspond to a number that is divisible by 7.

public class FileLicenseProvider : LicFileLicenseProvider { protected override bool IsKeyValid(string key, System.Type type) { int code = int.Parse(key.Substring(0, 3)) if (code <> 0) { if (Math.IEEERemainder(Code, 7) == 0) { return true; else { return false; } } else { return false; } } }

Advanced License Providers

Control licensing doesn't need to be based on LIC files. In fact, you can create any type of licensing scheme imaginable. You can even perform tremendously annoying tricks like only allowing controls to be registered to specific computers. To implement a custom licensing scheme, you need to create two classes: a custom license provider, and a custom license.

The custom license is the easiest ingredient. It simply derives from the base License class, overrides the LicenseKey property and Dispose() method, and adds properties for any required pieces of information. You also need to add a constructor that configures the license, as the LicenseKey property is read-only.

public class CustomLicense : License { private string key; public override string LicenseKey { get { return key; } } public CustomLicense(string key) { this.key = key; } public override void Dispose() { // This method must be overriden. } }

The custom LicenseProvider plays the same role as the LicFileLicenseProvider. It provides a GetLicense() method, which the .NET framework calls to validate the control. For example, when you use the LicenseManager.Validate() method in the constructor for the DirectoryTree control, .NET uses the LicenseProvider.GetLicense() method to retrieve the license.

In the GetLicense() method, you may want to examine whether the component is in design-time or runtime mode, and apply different rules. Additionally, you may want to return a valid license object, nothing at all, or throw a LicenseException to indicate that the control should not be created. The LicFileProvider throws a LicenseException to indicate when a LIC file is not valid.

The example that follows looks for a predefined registry entry at design time. At runtime, it first examines the current context, and then defaults to the registry if a compiled license key can't be found. The registry value is stored under a predefined company name, followed by the fully qualified name of the control. The key is validated as long as it matches the string "1234567890" and a CustomLicense object encapsulating this key is returned.

public class RegistryLicenseProvider : LicenseProvider { public override System.ComponentModel.License GetLicense( System.ComponentModel.LicenseContext context, System.Type type, object instance, bool allowExceptions) { string key; if (context.UsageMode == LicenseUsageMode.Runtime) { // Try to find key in current context. key = context.GetSavedLicenseKey(type, null); } // Always look in the registry at design time. // If the key wasn't found in the current context at runtime, // we can also look in the registry. // Another option might be to always allow the control at runtime, // and just restrict it at design time. if (key == "") { // A debugging hint (remove when you perfect the licensing scheme): MessageBox.Show("Performing registry lookup.", "RegistryLicenseProvider"); RegistryKey rk; rk = Registry.LocalMachine.OpenSubKey(@"SoftwareMyCompany" + type.ToString()); if (rk != null) { key = rk.GetValue("LicenseKey", ""); } // Save key in current context. if (key != "") { context.SetSavedLicenseKey(type, key); } } // Check if key is valid. if (!IsValid(key)) { if (!allowExceptions) { throw new LicenseException(type); } } // Return the license object. return new CustomLicense(key); } private bool IsValid(string key) { if (key == "1234567890") { return true; } else { return false; } } }

The GetLicense() method is provided with a fair bit of information, including the current LicenseContext, the type of the component that is requesting the license, and a reference to instance of the component. This means you can easily create a single LicenseProvider that could handle the licensing for all different types of controls. Custom licensing schemes are limited only by your imagination, and can become quite complex. The material presented here is only a basic introduction for what a component vendor might do.

 
Chapter 8 - Design-Time Support for Custom Controls
 
byMatthew MacDonald  
Apress 2002
Companion Web Site
 

The Last Word

This chapter covered a lot of ground with custom controls. The story doesn't end here, however. In the coming chapters you continue to look at how custom controls can solve all kinds of programming problems, and handle everything from data access to rich graphics. In all these cases, knowing how to match custom controls with an appropriate level of design-time support will simplify your life immensely. If you are planning to develop and sell your components, it's indispensable.

To see what other developers are creating, you can take your search online and begin working with the latest user interface elements. And if you develop your own controls, feel free to send me an email with the details. I can't critique your code creations, but I just might provide a link from this book's web site (http://www.prosetech.com) if there's enough interest….

 
Chapter 9 - Data Controls
 
byMatthew MacDonald  
Apress 2002
Companion Web Site
 

Data Controls

Категории