Cutting Edge
Custom Design-time Control Features in Visual Studio .NET
Dino Esposito
Code download available at:CuttingEdge0312.exe(165 KB)
Contents
The Design-time Architecture
Writing a Sample Control
Choosing the Proper Set of Attributes
Type Editors
Type Converters
Designer Verbs and Markup
ASP.NET Control Designers
Conclusion
In the Microsoft® .NET Framework, Windows® Forms and Web Forms controls have two distinct sets of functionality. Run-time capabilities dictate the behavior of the control when it is employed in production code and design-time capabilities let the programmer decide how the control will appear in a WYSIWYG environment and expose its programming interface. Both built-in and custom Windows Forms and ASP.NET controls can be architected to integrate with the Visual Studio® .NET designer. With a proper set of design-time features, a control can provide a realistic preview of its UI, offer a more user-friendly programming style, group properties by categories, and supply tips about them. In addition, a special design-time feature lets you add custom functions to the context menu of the control when it's placed on a form or a Web page created with Visual Studio .NET.
Both Windows Forms and Web Forms controls inherit some basic design-time capabilities from their base class—Control. Note that they both inherit from a base class called Control but the namespace of each is different—System.Windows.Forms versus System.Web.UI. The Control base class gives derived controls drag and drop capabilities and lets them interact with the Properties window in Visual Studio .NET. As a result, a control can be dragged and dropped onto a form. Furthermore, all controls, when selected within the designer, display their properties in the Properties window, making them editable.
The design-time behavior of a control can be enhanced by setting predefined attributes, by defining custom editors for properties, and by creating custom designers. In this column, I'll show how these types of design-time extensions can significantly improve the user experience with a sample Windows Forms control. Note that while both Control classes (from which all controls inherit) already provide a base set of capabilities that make controls usable within a visual designer, they don't necessarily provide the kind of rich user experience you're looking for.
Let's start with a quick overview of the overall design-time architecture of .NET Framework controls.
The Design-time Architecture
A typical design-time environment such as Visual Studio .NET contains at least a form designer and a property browser. In addition, a design-time environment will usually provide services that components access to customize their behavior at design time. These services include a set of functions that allow you to plug into the designer's internal mechanism. Components that can connect to the services exposed by the design-time environment are said to be designable components.
Figure 1** Control Inheritance **
In the .NET Framework, a designable component is simply a class that implements the IComponent interface. The hierarchy shown in Figure 1 shows that all controls—both Windows Forms and Web Forms—are designable components because all derive from classes that implement the IComponent interface. The IComponent interface has just one property, Site, which corresponds to an object that implements the ISite interface. The ISite interface defines four properties: Component, Container, DesignMode, and Name. The role of each property is fully described in Figure 2.
Figure 2 ISite Properties
Property | Description |
---|---|
Component | Gets the component associated with the class that implements the ISite interface. In a design-time control scenario, it represents the instance of the control being designed. |
Container | Gets the IContainer object associated with the class that implements the ISite interface. In a design-time control scenario, it represents the Visual Studio .NET component that hosts the designer. In Visual Studio .NET 2003, the class name is Microsoft.VisualStudio.Designer.Host. DesignerHost. |
DesignMode | Indicates whether the component is in design mode. Returns true. |
Name | Returns the name of the component associated with the site. In a design-time control scenario, it is the name of the control being designed. Name is a read/write property. |
The Site object binds a component to a container and enables communication between them. In addition, it provides a way for the container to manage its components and acts as a repository for container and component-specific information, such as the component name and attributes. The following code snippet shows how to use the Site object to detect whether the current control is being used at run time or design time:
if (this.Site != null && this.Site.DesignMode) { string buf = "'{0}' is hosted by {1}"; MessageBox.Show(String.Format(buf, this.Site.Name, this.Site.Container.ToString())); } else MessageBox.Show("The control works in a run-time context.");
Windows Forms controls derive from the Component class (defined in the System.ComponentModel namespace), which implements the IComponent interface. Web Forms controls, in contrast, have the IComponent interface implemented directly by the Control class (defined in the System.Web.UI namespace). The IComponent interface extends the IDisposable interface and implements the Dispose method. The Dispose method is available to components to release external resources explicitly. You should note that the Dispose method represents a deterministic way to free resources, unlike the default nondeterministic cleanup that is performed by the garbage collector.
The Visual Studio .NET design-time architecture requires that the design-time capabilities of a control not be implemented within the control itself but in separate classes. Design-time classes and controls are then linked declaratively using attributes. Separating run-time and design-time code has two main advantages. First, the code that creates the control has a smaller footprint, making it more manageable at run time. Second, insulating design-time functionality in external classes results in a much more flexible and extensible architecture, as you'll see in a moment. The various types of design-time functionality are summarized in Figure 3.
Figure 3 Design-time Functionality
Extension | Description |
---|---|
Attribute | A bunch of predefined attributes let you instruct the host environment about the description to show for a property, or the category under which the property should be displayed. |
Designer | A class that takes care of the appearance and behavior of the control when hosted in Visual Studio .NET. You can use this class to add new context menu verbs and, for ASP.NET controls, to explicitly indicate the HTML code to show in the designer. |
Type converter | A class that performs conversions to represent values of arbitrary types as strings. Used by the Properties window. |
UI type editor | A class that provides a convenient way of editing the values of a certain type. For example, a UI type editor is used to select a color from a palette. |
You can extend the base design-time support for controls in various ways, the simplest of which is mostly declarative and consists of setting a few metadata attributes such as Browsable, Category, and Description. Achieving more advanced design-time support requires additional work and new classes. To do this, you need to import a few namespaces, including System.ComponentModel.
Writing a Sample Control
To see the design-time functionality of the .NET Framework in action, let's begin by building a custom Windows Forms control that contains many of the features that justify the extensive use of design-time features. The user control I'm going to write will be named FileChooser and will consist of Label, Textbox, and Button controls. The purpose of the component is to let users select a file name. The file name can be typed directly in the Textbox or the button can be clicked to show the standard File Open dialog to navigate to a file. The Label control gives the Textbox a caption. When it runs, the sample app shown in the designer in Figure 4 launches an "Open" dialog.
Figure 4 Sample App in Visual Studio .NET Designer
The control is built around an internal method named RenderControl (not to be confused with the similarly named method of ASP.NET controls). The method takes care of moving the constituent controls around to make them fit into the assigned area—the area reserved for the control on the hosting form. Page and form developers decide the location (site) of each embedded control by resizing the control through drag and drop or by setting the width and height properties in the Properties window.
RenderControl ensures that the button has a fixed width and text (the ellipsis). The Textbox is the size of the site minus the button width. The label can be hidden or displayed above or below the Textbox. A specific property indicates the correct location. Font, alignment, and text of the label are grouped in a custom class—ChooserCaption—exposed through a Caption property. The name of the selected file is returned through a string property named FileName. Finally, a Boolean ReadOnly property restricts the control to accepting the name of the selected file only from the File Open common dialog. In this case, the Textbox control is marked as read-only. Figure 5 lists the properties supported by the control. The characteristics of those properties require different types of design-time features.
Figure 5 Sample Control Properties
Property | Description | Design-time Extension |
---|---|---|
Caption | Property of type ChooserCaption that represents graphical attributes such as the text, font, and alignment of the label. | Type converter |
CaptionLocation | Custom enum type that indicates the location of the label with respect to the Textbox. | Attribute |
FileName | Gets and sets the name of the selected file. | UI type editor |
ReadOnly | Indicates whether the Textbox is read-only. In this case, the file can be selected only through the File dialog box. | Attribute |
TextBackColor | Indicates the background color of the Textbox. | Attribute |
TextForeColor | Indicates the foreground color of the Textbox. | Attribute |
Choosing the Proper Set of Attributes
By default, all public control properties implemented with an explicit pair of get/set accessors show up in the Visual Studio .NET Properties window. The following example demonstrates the implementation of the TextForeColor property. As you can see, the property simply mirrors the ForeColor property of the constituent Textbox (see Figure 6).
Figure 6
public Color TextForeColor { get {return theFileName.ForeColor;} set {theFileName.ForeColor = value;} }
The way in which properties appear in the Properties window depends on the attributes declaratively assigned to the properties in the control's source code. In particular, the Browsable attribute determines whether or not a control is displayed. The most commonly used attributes are listed in Figure 7. The following attributes give the property a description and bind it to the Appearance category of the Properties window. The Browsable attribute is not strictly necessary as all properties with get/set accessors show up by default. Use the Browsable(false) attribute only to prevent a property from being displayed.
Figure 7 Commonly Used Attributes
Attribute | Description |
---|---|
Bindable(bool) | Indicates whether a property can be bound to data. |
Browsable(bool) | Indicates whether a property or an event should be displayed in the Properties window. |
Category(name) | Provides the name of the category in which a property or an event belongs. This allows for logical grouping of properties and events. |
DefaultValue(value) | Used to provide a default value for a control property. If the property doesn't have a simple type, a type converter is needed. |
Description(text) | Provides a description of a property or an event. The text is displayed at the bottom of the Properties window when the user selects a property or event. |
Editor(type) | Specifies the editor to use for modifying a property in a visual designer. |
Localizable(bool) | Indicates that a property can be localized. Any properties that have this attribute are automatically persisted into the resources file when a user chooses to localize a form. |
ParenthesizePropertyName(bool) | Indicates that a property should be displayed with parentheses around its name. |
Figure 8** Properties Windows **
Figure 8 shows the effect of the following attributes.
[Browsable(true)] [Category("Appearance")] [Description("Gets and sets the foreground color of the textbox")]
The TextForeColor property falls under the Appearance category when the categorized view is selected in the Properties window. Any text that is associated with the Description attribute is displayed in the bottom-most area of the window when the property is selected.
The attributes in Figure 7 characterize control properties. A few specific attributes exist for the control class, too. They are DefaultProperty and DefaultEvent, as shown here:
[DefaultProperty("FileName")] [DefaultEvent("FileSelected")] public class FileChooser : System.Windows.Forms.UserControl { ... }
The DefaultProperty attribute indicates the default property of the control. When an instance of the control is selected in the designer, the specified property is automatically selected and becomes visible in the Properties window. The properties grid scrolls up enough to show the default property in the current view. The DefaultEvent attribute denotes the default event (if any) defined for the control. A handler for this event is automatically added to the code-behind class of the host form or page whenever you double-click on the control in the designer. For example, the default event for a form is Load; Click for a Button; and so on. In addition, Web server controls also feature another class-level attribute—ToolboxData.
[ToolboxData("<{0}:MyDiv runat=server> </{0}:MyDiv>")] public class MyDiv : WebControl { ... }
The attribute indicates the name of the tag generated for a custom control when it is dragged from a toolbox in the Visual Studio .NET designer. Based on the previous code snippet, the following markup is added to the underlying host Web page:
<msdn:MyDiv runat=server></msdn:MyDiv>
The {0} placeholder in the code previous to this one references the value of the TagPrefix attribute in the @Register directive used to link the custom Web control to the page. While defining the ToolboxData attribute you can add as many HTML attributes as needed to the output markup.
In Figure 8, you can see a dialog box just below the name of the property in the Properties window. The content of the dialog box lets you pick a color to assign as the new property value. In other words, the dialog box acts as a property-specific user interface to edit the value. A few types in the .NET Framework have a custom user interface editor, notably the Color, Font, Image, Boolean, and enum types. Whenever a property of any of these types shows up in the Properties window, Visual Studio .NET automatically binds it to a default, built-in editor. For example, properties of type Color are edited through the services of the ColorEditor class whereas font values are edited using FontEditor.
Booleans and values that belong to an enumeration get special treatment from the property grid and are rendered using a combobox with all possible values. What each editor does depends on the characteristics of the type it represents. For example, ImageEditor provides a UI for selecting an image file. Starting the editor displays an open file dialog box initially filtered to select all image files. In all these cases, you don't need to do anything to get the aforementioned services. By setting the Editor attribute on the property, though, you can force an editor-to-property binding.
Type Editors
All type editors inherit from the base class UITypeEditor. A type editor provides a UI to edit a property in a more user-friendly way. The .NET Framework comes with a handful of UI type editors but others can be created using the UITypeEditor class. The FileChooser control has Color and Boolean properties for which Visual Studio .NET automatically selects an appropriate UI editor. The FileName property is a string that represents a file name; users can enter the value by either manually typing in the property grid or using the associated editor. The .NET Framework even provides an editor for properties that represent a file name—the FileNameEditor class in the Sys-tem.Windows.Forms.Design namespace.
[Browsable(true)] [Description("...")] [Editor(typeof( System.Windows.Forms.Design.FileNameEditor), typeof(UITypeEditor))] public string FileName { ... }
However, I'll pretend I don't know about it and create a clone of the UI editor programmatically. I'll call it the MyControls.FileNameEditor class.
Insulating the design-time classes in a separate assembly is a good programming practice, but at a minimum write your design-time code in a distinct file within the project. If a single assembly is used, you might want to mark the design-time classes as internal so that they won't be callable from outside the control's assembly. On the other hand, if you use a separate assembly, design-time classes must be public.
A UI type editor class is a class that inherits from UITypeEditor and overrides at least the GetEditStyle and EditValue methods. Figure 9 illustrates the full source code of the custom FileNameEditor class. A UI type editor is basically a dialog box whose controls let you define the new value for the particular property. The GetEditStyle method indicates the style of the requested user interface—modal or dropdown. The values are grouped in an enum type named UITypeEditorEditStyle. Modal displays a modal dialog box; DropDown displays a dialog box that is dismissed as soon as the window loses focus (like the list of a combobox). If you opt for modal, the property is characterized by an ellipsis button in the property grid; otherwise, a combobox-like button appears. No matter what the button looks like, clicking it always invokes the EditValue method.
Figure 9 Custom FileNameEditor Class
internal class FileNameEditor : UITypeEditor { public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context) { return UITypeEditorEditStyle.Modal; } public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) { // value is the value in the PropertyBox FileChooser parent = (FileChooser) context.Instance; // PickUpFile is a public method on the control that // invokes the File Open common dialog string fileName = parent.PickUpFile(); if (fileName == String.Empty) return value; return fileName; } }
The EditValue method is responsible for popping up the dialog box and for any operation necessary to get and store the new value. Typically, EditValue displays the dialog, captures the new value, and returns it. Should you need to invoke a method on the designable control, the context parameter provides a reference to it. The current value of the property is passed to EditValue through the value parameter (see Figure 9).
Another feature that UI type editors may support is manual painting of the value. The GetPaintValueSupported virtual method indicates whether this editor supports painting a representation of an object's value. Both the ImageEditor class and the ColorEditor class provide a graphical representation of the value—a little icon for ImageEditor and a rectangle filled with the chosen color for ColorEditor. If you intend to do the same in your control, start by overriding GetPaintValueSupported and make it return true. Next, override PaintValue and provide your own painting code.
Type Converters
What if you want your control to contain a property of a custom type? At first, this task doesn't look particularly difficult. You create a UI type editor, design the dialog box to fit all the properties, and return an instance of the type from EditValue. Type editors, though, have been introduced for simple types such as strings, Booleans, enum values, and colors. The values you assign to the various properties of a custom type are to be serialized when the form (or page) class is persisted to disk. This is not a problem for simple types, but what about custom types? Class serialization could be the answer but Visual Studio .NET supports a better, lightweight alternative—type converters.
A type converter is a class that converts a custom type to and from a string. To write a type converter, derive a class from TypeConverter and override the ConvertFrom and ConvertTo methods. ConvertFrom parses the specified string and converts it to an instance of the target type. The latter does the reverse and serializes an instance of a type to a string. Let's see how it works for the Caption property implemented through the ChooserCaption class. The class features three properties, as shown in Figure 10.
Figure 10 ChooserCaption Class
[TypeConverter(typeof(ChooserCaptionConverter))] public class ChooserCaption { public ChooserCaption() { CaptionText = "File"; FontBold = true; Alignment = ContentAlignment.TopLeft; } [Description("...")] public string CaptionText { ... } [Description("...")] public bool FontBold { ... } [Description("...")] public ContentAlignment Alignment { ... } }
The string value that represents a custom type is a string of comma-separated values, each of which represents a property. The type converter decides the order, number, and text of the serialized values. Figure 11 shows the full source code of the sample type converter for the Caption property. The ConvertFrom method receives a string of comma-separated values and splits its contents. Each part is then used to set the corresponding property of the custom class. The following lines of code show how to set the CaptionText property.
ChooserCaption caption = new ChooserCaption(); string serializedData = (string) value; string[] parts = serializedData.Split(','); caption.CaptionText = parts[0];
Figure 11 Type Converter
internal class ChooserCaptionConverter : ExpandableObjectConverter { public ChooserCaptionConverter() { } // **************************************************** // Convert the specified text sequence to a ChooserCaption // instance public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value) { // Default instance if (value == null) return new ChooserCaption(); // Deserialize to ChooserCaption from a string // representation of the type if (value is string) { // Get the source string string serializedData = (string) value; if (serializedData.Length == 0) return new ChooserCaption(); // ********************************************* // The string representation is a collection of 3 // strings: // + text // + bold (Boolean) // + alignment (ContentAlignment) // ********************************************* // Split the string into parts (must be 3 parts— // one per each property) string[] parts = serializedData.Split ( culture.TextInfo.ListSeparator[0]); if (parts.Length != 3) throw new ArgumentException( "Invalid ChooserCaption object", "value"); // Create a new ChooserCaption object ChooserCaption caption = new ChooserCaption(); // Populate the newly created object with the // deserialized data // Part 1 is a string representing the Text // property caption.CaptionText = parts[0]; // Part 2 is a Boolean value representing the bold // attribute TypeConverter boolConv = TypeDescriptor.GetConverter(typeof(bool)); caption.FontBold = (bool) boolConv.ConvertFromString(context, culture, parts[1]); // Part 3 is a string representing a // ContentAlignment value string align = parts[3]; caption.Alignment = (ContentAlignment) Enum.Parse(typeof(ContentAlignment), align); return caption; } return base.ConvertFrom (context, culture, value); } // ***************************************************** // ***************************************************** // Convert a ChooserCaption to a string public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType) { // Ensure the value is a ChooserCaption object if (value != null) { if (!(value is ChooserCaption)) throw new ArgumentException( "Invalid ChooserCaption object", "value"); } // If null, return the empty string if (destinationType == typeof(string)) { if (value == null) return String.Empty; // Get the object to serialize and necessary // converters ChooserCaption caption = (ChooserCaption) value; // Create the string string[] parts = new string[3]; parts[0] = caption.CaptionText; parts[1] = caption.FontBold.ToString(); parts[2] = caption.Alignment.ToString(); return String.Join(culture.TextInfo.ListSeparator, parts); } return base.ConvertTo(context, culture, value, destinationType); } // ***************************************************** }
The second segment in the serialized string represents a Boolean value. That string is first converted to a Boolean and then assigned to the FontBold property. Similarly, the text representation of the ContentAlignment value is converted to the proper type and then assigned. The final instance of the ChooserCaption class is then returned by the method.
The ConvertTo method takes the current instance of the ChooserCaption class and serializes its content to a string of comma-separated values:
ChooserCaption caption = (ChooserCaption) value; string[] parts = new string[3]; parts[0] = caption.CaptionText; parts[1] = caption.FontBold.ToString(); parts[2] = caption.Alignment.ToString(); return String.Join(culture.TextInfo.ListSeparator, parts);
If you inherit your class from TypeConverter, the property appears in the Properties window as an editable string of comma-separated values. If you use ExpandableObjectConverter as the base class, the Caption property becomes an expandable object (like the default Font property) and once expanded shows all the child attributes serialized to the type converter string. You can edit values either as a string of comma-separated values (not recommended) or individually by editing each single property.
When defining a class used for a design-time property, you normally use the TypeConverter attribute along with the DesignerSerializationVisibility serialization attribute:
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] [TypeConverter(typeof(ChooserCaptionConverter))] public class ChooserCaption { ... }
This attribute is typically set to Content. It indicates the type of persistence to use when serializing the property at design time. Content indicates that each public, non-hidden sub-property of the object will be serialized.
Designer Verbs and Markup
A third type of extension is a custom designer. A designer is a class that provides all the logic necessary to interact with the host environment and lets users build the appearance and behavior of a component at design time. All designers implement the IDesigner interface. The .NET Framework supplies a set of designers for specific types of components and controls. By writing a custom designer, you can add custom initialization code for a component in design mode. You could also configure and create components within a project and, quite interestingly, add menu items to the context menu of a control. Have you ever noticed the AutoFormat link button that magically appears in the Properties window when a DataGrid control is selected in the designer? If you want to add a similar feature to your controls, be prepared to write a custom designer. A custom designer is declaratively bound to the control using the Designer attribute:
[Designer(typeof(FileChooserDesigner))] public class FileChooser : System.Windows.Forms.UserControl { ... }
The FileChooserDesigner class inherits ControlDesigner, one of the built-in designer classes in the .NET Framework. It provides all the logic needed to manage controls in Visual Studio .NET. If you want to add new verbs to the control, you override the Verbs property. A verb is an action that can be performed at design time on an instance of the control. Verbs appear in two places—in the context menu and in the Properties window just below the property grid. In the property grid, verbs are rendered as link buttons and clicking them causes an event handler to run. The Verbs property is a collection of DesignerVerb objects and is empty by default. The override in Figure 12 adds a couple of verbs to the FileChooser control—"Auto Format" and "Something Else".
Figure 12 Override Verbs
public override DesignerVerbCollection Verbs { get { if (m_Verbs == null) { // Create and initialize the collection of verbs m_Verbs = new DesignerVerbCollection(); m_Verbs.Add( new DesignerVerb("Auto Format", new EventHandler(OnAutoFormatSelected)) ); m_Verbs.Add( new DesignerVerb("Something Else", new EventHandler(OnSomethingElseSelected)) ); } return m_Verbs; } }
A designer verb is characterized by text (the hyperlink) and an event handler. The event handler is invoked when the user clicks the verb on the context menu or the property grid. If you look back to Figure 4, you can see the new verbs and the dialog box displayed by the AutoFormat verb. The same two verbs are added to the context menu that is displayed when you right-click on the FileChooser control in Visual Studio .NET. The effect for the code is the same—the same event handler is always invoked.
The event handler displays a dialog box, captures a few new values, and reflects them to the control being designed. The code in Figure 13 shows the behavior of the event handler associated with the AutoFormat verb.
Figure 13 AutoFormat Event Handler
void OnAutoFormatSelected(object sender, EventArgs args) { // Display a message AutoFormatDialog dlg = new AutoFormatDialog(); DialogResult result = dlg.ShowDialog(); if (result == DialogResult.OK) { // Update the control being designed with new values FileChooser ctl = (FileChooser) this.Control; ctl.ReadOnly = dlg.ReadOnly; ctl.CaptionLocation = dlg.CaptionLocation; ctl.Caption.Alignment = dlg.TextAlign; } }
As you can see, a few public properties have been defined on the AutoFormatDialog class—the class that represents the popup dialog—to expose the values to map on the FileChooser instance under design. The AutoFormat dialog box mimics the layout of the similar dialog defined for DataGrid controls. What's the easiest way of providing a preview of the control's appearance? That's easy. You just use another instance of the same FileChooser control and set its properties as suggested by the selected format.
For the icing on the cake, let's spend a few minutes looking at an interesting feature—designer transactions. A designer transaction groups a series of design-time actions to enable changes to be undone. In addition, when multiple actions have been performed, transactions improve performance by deferring updates to the display until the completion of the transaction. To perform a transaction within your designer, call CreateTransaction on the designer host object. You get a reference to the host by calling GetService—a protected member inherited from ComponentDesigner:
IDesignerHost host = (IDesignerHost) GetService(typeof(IDesignerHost)); DesignerTransaction t = host.CreateTransaction();
CreateTransaction returns a DesignerTransaction object that can be used to control the transaction, commit it, or roll it back. All the transacted actions should be executed within a try block. To complete the transaction, call Commit from within a finally block. In C#, you can use the using statement rather than try...finally. The using statement defines a scope at the end of which the transaction object will be disposed:
using (host.CreateTransaction()) { // actions go here }
To cancel and attempt to roll back a transaction before it has been committed, call the Cancel method. To undo actions which occurred as part of earlier transactions, you must use the undo command provided by the Visual Studio .NET IDE.
ASP.NET Control Designers
What I've discussed so far (attributes, type editors and converters, and verbs) applies to Windows Forms and Web Forms controls as well. Web Forms controls have a different set of predefined type editors but that's all; there's no other significant difference between the two. ASP.NET designers, though, have a larger set of methods and features. In particular, you can use a different rendering algorithm for design time and run time, and even disable resizing for controls when they're hosted in a Web page designer. The System.Web.ControlDesigner class provides a few virtual methods that relate to the markup code shown in the designer. The list includes methods such as GetDesignTimeHtml, GetEmptyDesignTimeHtml, and GetErrorDesignTimeHtml. GetDesignTimeHtml gets the HTML that is used to represent the control at design time. The other two methods get the HTML that is used to represent an empty control and the exception that occurred at design time. Resorting to these methods makes sense, especially when you're designing a data-bound control and need to provide sample data. For example, when its AutoGenerateColumns is true, the DataGrid control displays a number of columns with sample data. For an example of custom design-time HTML for ASP.NET controls, see my book Programming Microsoft ASP.NET (Microsoft Press®, 2003). The source code for the control described in the book is available on the Microsoft Press Web site.
Conclusion
If Visual Studio .NET is your development tool of choice, then chances are good that you'll find yourself assembling forms or pages using a visual designer. Once you choose the controls from a palette, drop them onto a design surface, and instantiate them, they can be configured declaratively through the Properties window. Built-in controls provide advanced features when hosted in Visual Studio .NET that make for a rich user experience. This is not always the case for custom controls. Both Windows Forms and ASP.NET custom controls need additional code to integrate well with Visual Studio .NET. In this column, I've discussed the few things you must know in order to build effective design-time features. This month's download, which can be found on the MSDN® Magazine Web site, contains the code for the FileChooser Windows Forms control (see the link at the top of this article).
Send your questions and comments for Dino to cutting@microsoft.com.
Dino Esposito is an instructor and consultant based in Rome, Italy. He is the author of Building Web Solutions with ASP.NET and ADO.NET and Applied XML Programming for .NET, both from Microsoft Press. Dino recently published Programming ASP.NET for Microsoft Press. Get in touch with him at cutting@microsoft.com.