Smart Tags

Simplify UI Development with Custom Designer Actions in Visual Studio

Michael Weinhardt

This article is based on a prerelease version of Visual Studio 2005. All information contained herein is subject to change.

This article discusses:

  • The role of designer actions in Visual Studio
  • Designer action property and method items
  • How you can build your own custom designer actions and accompanying smart tags
This article uses the following technologies:
Visual Studio 2005

Code download available at:DesignerActions.exe(194 KB)

Contents

An Introduction to Designer Actions
Building a Designer Action List
Creating Designer Action Property Items
Creating a Custom Designer
Rendering Designer Action Property Items
Design-Time-Only Designer Action Property Items
Multivalue Designer Action Property Items
Designer Action Method Items
Adding Designer Action Method Items
Toggling the Designer Action Method Item Display Name
Automatically Show the Smart Tag Panel
Categories and Descriptions
Headers and Labels
Where Are We?

The Properties window, which has been a mainstay of the Windows® Forms design-time experience, supports the configuration of an entire component or control from one location. Visual Studio® 2005 complements this experience with a new feature, known as smart tags, which exposes key configurations right on the design surface to enhance overall design-time productivity. Many Windows Forms 2.0 components expose smart tags by utilizing a subset of the Microsoft® .NET Framework 2.0 known as Designer Actions. In the same way, you can add smart tags to your own custom components and reap the same benefits.

An Introduction to Designer Actions

When components that support smart tags are selected on the Windows Forms design surface, a smart tag anchor is displayed at the top-right edge of a component. The anchor is a button that, when clicked, opens the smart tag panel. A smart tag panel is a designer-managed UI that exposes component configurations as a set of smart tag entries. Figure 1 illustrates these key elements.

Figure 1 Smart Tag Anchor, Panel, and Tasks

Figure 1** Smart Tag Anchor, Panel, and Tasks **

In the designer action infrastructure of Visual Studio 2005, smart tag entries are known as designer action items and are shuttled around the design time in groups known as designer action lists. If a component requires smart tag support, it must pass at least one designer action list containing at least one designer action item to the Windows Forms designer. The same designer action list is then passed to the smart tag panel, which converts each designer action item into an equivalent visual task.

Building a Designer Action List

Since designer action items need to be packaged within designer action lists, the best way to begin adding smart tag support to your component is by building a designer action list. The designer action infrastructure implements a special class to represent designer action lists, appropriately called DesignerActionList. Like all designer action classes, DesignerActionList is located in the System.ComponentModel.Design namespace. Physically, however, they reside in the System.Design.dll assembly which you'll need to reference from your project before going any further.

The DesignerActionList class is designed to associate one or more designer action items with a single component. The association is made via its constructor, which expects a component's IComponent reference that is also exposed from the read-only Component property:

public class DesignerActionList { public DesignerActionList(IComponent component); public IComponent Component { get; } ... }

DesignerActionList isn't a list in the sense that it implements IList or ICollection. Instead, it returns a list of designer action items stored in a DesignerActionItemCollection from its GetSortedActionItems method. The way you return your own list of designer action items is to derive from DesignerActionList, override GetSortedActionItems, and create, fill, and return a DesignerActionItemCollection instance (if you don't override it, the default implementation of GetSortedActionItems will use reflection to wrap all public methods and properties in DesignerActionItems and return them sorted alphabetically):

public class ClockControlDesignerActionList : DesignerActionList { ... public override DesignerActionItemCollection GetSortedActionItems() { // Create list to store designer action items DesignerActionItemCollection actionItems = new DesignerActionItemCollection(); ... // Fill list of designer action items // Return list of designer action items return actionItems; } }

DesignerActionItemCollection is a strongly typed collection that you fill with one or more DesignerActionItem objects. DesignerActionItem is an abstract class that captures functionality common to all designer actions. However, because DesignerActionItem is abstract, it can't be instantiated directly and, even if it could, it is too generic to be useful. Instead, the designer action infrastructure provides several DesignerActionItem derivations, each of which is specialized to support a particular style of designer action item. One of those derivations, the DesignerActionPropertyItem, models what is possibly the most common type of item displayed from a smart tag panel—a property entry.

Creating Designer Action Property Items

As an example, I'll be using the ClockControl that Chris Sells and I developed in our two-part series from the April 2003 and May 2003 issues of MSDN®Magazine. ClockControl had a Face property, which specifies whether the ClockControl's face is rendered as analog, digital, or both. Because it's one of the properties most likely to be configured at design time, it is an ideal smart tag candidate.

To turn it into a designer action property item, you need to create a DesignerActionPropertyItem instance and add it to DesignerActionItemCollection, like so:

public class ClockControlDesignerActionList : DesignerActionList { ... public override DesignerActionItemCollection GetSortedActionItems() { ... // Add Face designer action property item actionItems.Add(new DesignerActionPropertyItem("Face", "Face")); ... } ... }

The DesignerActionPropertyItem class constructor expects two string arguments: member name and display name. Member name is the name of a property that's exposed by the custom DesignerActionList. Display name is a text value that the smart tag panel displays as a smart tag task's label.

The primary reason for the DesignerActionList to provide a property implementation, rather than using a component's own implementation, is that the smart tag panel is geared to operate on DesignerActionList objects rather than directly with the components themselves. As such, DesignerActionLists must implement a property that exposes the component's property, by proxy, for the smart tag panel to read from and write to. The role of the proxy property is illustrated by the diagram in Figure 2.

Figure 2 Proxy Property Implementation Exposed from DesignerActionList

Figure 2** Proxy Property Implementation Exposed from DesignerActionList **

In light of this requirement, there is another consideration to make. The proxy property should not directly set a component's properties using code like this:

public class ClockControlDesignerActionList : DesignerActionList { ... // Face property public ClockFace Face { get { return this.ClockControl.Face; } set { this.ClockControl = value; } } ... }

As outlined in the documentation, such code circumvents design-time services like the Properties window refreshing to reflect the changed property, or supporting undo for property. You can avoid this by coding the property's set accessor to use a property descriptor, as shown in Figure 3. You could use the Windows Forms designer's component change service to set these changes, although the technique that is suggested in the documentation is arguably much more concise.

Figure 3 Safe Implementation of the Face Property

public class ClockControlDesignerActionList : DesignerActionList { ... // Face property public ClockFace Face { get { return this.ClockControl.Face; } set { SetProperty("Face", value); } } // Helper method to safely set a component's property private void SetProperty(string propertyName, object value) { // Get property PropertyDescriptor property = TypeDescriptor.GetProperties(this.ClockControl)[propertyName]; // Set property value property.SetValue(this.ClockControl, value); } // Helper property to acquire a ClockControl reference private ClockControl ClockControl { get { return (ClockControl)this.Component; } } }

Both the proxy Face property and the GetSortedActionItems method comprise the minimum implementation required by a designer action list. GetSortedActionItems will be called from the smart tag panel when it opens and when it is refreshed, in both cases retrieving the single designer action property item and displaying it as an equivalent smart tag task. The initial value for the smart tag property is retrieved from the component's Face property through the ClockControlDesignerActionList class's proxy property, and vice versa to set the Face property if it's changed from the smart tag panel.

Of course, the problem is that we haven't yet passed ClockControlDesignerActionList to the Windows Forms designer and, consequently, ClockControl won't even be provided designer action support. This little hole is filled by a custom designer.

Creating a Custom Designer

Designer actions are classified as design-time-only functionality, which means they are provided to and used by a component solely from within the design-time environment. If you're familiar with the design-time infrastructure in general, you'll be aware that it is the responsibility of the designer class to provide design-time-only functionality such as this. By default, all components are provided a default designer when they are hosted in the Windows Forms designer, unless they use their own custom designers. A custom designer is a class that implements the IDesigner interface, although most derive from .NET Framework-provided IDesigner base implementations specific to the component type, including ComponentDesigner for components and ControlDesigner for controls. A custom designer is associated with a component using the DesignerAttribute. In the original implementation of ClockControl, ClockControlDesigner was created to add a design-time-only property to the ClockControl. Following are the key implementation details:

public class ClockControlDesigner : ControlDesigner { ... } [Designer(typeof(ClockControlDesigner))] public class ClockControl : Control, ... { ... }

With the advent of the .NET Framework 2.0 and smart tag support, the ComponentDesigner class was updated with the new read-only ActionLists property, shown here:

public class ComponentDesigner : IDesigner, ... { ... public virtual DesignerActionListCollection ActionLists { get; } ... }

The ActionLists property is of type DesignerActionListCollection, an object that stores one or more DesignerActionList objects. Although discussion of multiple designer action lists are beyond the scope of this article, this support for them allows you to break a single custom designer action list into several discrete versions, which you may consider if the original is becoming too large to easily maintain, or if you need to selectively show subsets of designer action items based on a component's design-time context.

By default, the base ComponentDesigner class's ActionList property returns an empty DesignerActionListCollection, which is viewed by the Windows Forms designer as "Smart Tags Not Required" for the component in question. Consequently, you'll need to override ActionList and return your own DesignerActionListCollection, to which you'll add your custom DesignerActionList implementations. Since ClockControlDesigner indirectly derives from ComponentDesigner via ControlDesigner, you can override ActionLists directly, as shown in Figure 4.

Figure 4 Overriding ActionLists

public class ClockControlDesigner : ControlDesigner { ... public override DesignerActionListCollection ActionLists { get { // Create action list collection DesignerActionListCollection actionLists = new DesignerActionListCollection(); // Add custom action list actionLists.Add( new ClockControlDesignerActionList(this.Component)); // Return to the designer action service return actionLists; } } ... }

That's all the implementation you need to be able to edit a property from the smart tag panel. After rebuilding the updated ClockControl solution and then dragging the new ClockControl onto the form, you should be able to edit the Face property from the smart tag panel for the ClockControl, as shown in Figure 5.

Figure 5 Face Designer Action Property Item

Figure 5** Face Designer Action Property Item **

While you can't influence what's displayed as the smart tag panel title, the Windows Forms designer uses a reasonable default with the following naming convention: "ComponentTypeName Tasks". The Windows Forms designer also takes care of determining what controls are used to provide display and editing of designer action property items.

Rendering Designer Action Property Items

From Figure 5, you can see that the smart tag panel renders enumeration types, like the Face property, as dropdown lists. The heuristics exercised by the smart tag panel to translate other property types to controls includes the following:

  • If a property has an associated UI type editor, as specified automatically by the property type, or by manually specifying one by applying the EditorAttribute to the proxy property, the editor is shown.
  • If a property is a Boolean, a checkbox is shown.
  • Properties other than enumerations and Booleans are displayed as textboxes. If a property is of a type that doesn't support conversion from a string, it's displayed as read-only text sourced from the type's ToString method.

While you have no control over this process, beyond choosing a property's type, you can add the icing to the heuristics cake with the same special design-time attributes to provide enhanced property editing support. One of these is the EditorAttribute, and it allows developers to associate a specialized UI with a property. The ClockControl library already contains a UI type editor, FaceEditor, designed specifically for properties of type ClockFace. Since the Face property is of such a property type, its ClockControl implementation can, and should, be adorned with EditorAttribute to associate the FaceEditor with it. Unfortunately, the smart tag panel doesn't know about it because it refers to the ClockControlDesignerActionList class's proxy Face property. However, the situation is quickly rectified by adding the EditorAttribute to the proxy Face property, as shown here:

// Face proxy property [Editor(typeof(FaceEditor), typeof(UITypeEditor))] public ClockFace Face { get { return this.ClockControl.Face; } set { SetProperty("Face", value); } }

FaceEditor provides a dropdown style of UI type editor, but the same technique can also be used with modal UI type editors, such as the DigitalTimeFormatEditor that ClockControl associates with the DigitalTimeFormat property, as shown in Figure 6. A modal UI type editor behaves the same way from the smart tag panel as it would from the Properties window.

Figure 6 Using DigitalTimeFormatEditor

public class ClockControlDesignerActionList : DesignerActionList { ... public override DesignerActionItemCollection GetSortedActionItems() { ... // Add DigitalTimeFormat designer action property item actionItems.Add(new DesignerActionPropertyItem( "DigitalTimeFormat","Digital Time Format")); ... } ... [Editor(typeof(DigitalTimeFormatEditor), typeof(UITypeEditor))] public string DigitalTimeFormat { get { return this.ClockControl.DigitalTimeFormat; } set { SetProperty("DigitalTimeFormat", value); } } ... }

If a proxy property is one of the .NET Framework 2.0 intrinsic types, such as DateTime, that have UI type editors associated with them, the smart tag panel will automatically allow access to UI type editors. This is the case for ClockControl's BackupAlarm and PrimaryAlarm properties.

While adding a UI type editor to a designer action property item is relatively straightforward, more exotic scenarios are supported and I'll take a look at some of them now.

Design-Time-Only Designer Action Property Items

The ClockControl's original designer exposed the design-time-only ShowBorder property, shown here:

public class ClockControlDesigner : ControlDesigner { ... // Provide implementation of ShowBorder to provide // storage for created ShowBorder property [CategoryAttribute("Design")] [DesignOnlyAttribute(true)] [DefaultValueAttribute(true)] [DescriptionAttribute("Show/Hide a border at design-time.")] public bool ShowBorder { get; set; } ... }

Because it's implemented on a designer rather than a component, turning ShowBorder into a designer action property item is a little different than what you've seen, although the creation of a DesignerActionPropertyItem for it is the same:

public class ClockControlDesignerActionList : DesignerActionList { ... public override DesignerActionItemCollection GetSortedActionItems() { ... // ShowBorder designer action property item actionItems.Add( new DesignerActionPropertyItem("ShowBorder", "Show Border")); ... } ... }

The difference here is that the proxy ShowBorder property will need to read from and write to ClockControlDesigner's property implementation. First this means acquiring a reference to ClockControlDesigner from ClockControlDesignerActionList. Unfortunately, DesignerActionList doesn't provide this support natively, but it's quite simple to add:

public class ClockControlDesignerActionList : DesignerActionList { ... // Helper method to acquire a ClockControlDesigner reference private ClockControlDesigner Designer { get { IDesignerHost designerHost = (IDesignerHost) this.ClockControl.Site.Container; return (ClockControlDesigner) designerHost.GetDesigner(this.ClockControl); } } ... }

Now, the proxy ShowBorder property can easily interact with the ClockControlDesigner class's actual ShowBorder property:

public class ClockControlDesignerActionList : DesignerActionList { ... // ShowBorder proxy property public bool ShowBorder { get { return this.Designer.ShowBorder; } set { this.Designer.ShowBorder = value; } } ... }

You may have noticed that this proxy property doesn't use the SetProperty helper introduced earlier. Instead, it leans on the actual ShowBorder implementation's use of the design-time's component change service to achieve the same result (see Figure 7).

Figure 7 ShowBorder Implementation

public class ClockControlDesigner : ControlDesigner { ... public bool ShowBorder { get { return _showBorder; } set { // Change property value PropertyDescriptor property = TypeDescriptor.GetProperties( typeof(ClockControl))["ShowBorder"]; this.RaiseComponentChanging(property); _showBorder = value; this.RaiseComponentChanged(property, !_showBorder, _showBorder); ... } } ... }

The only thing left to do is rebuild the solution, with the result shown in Figure 8.

Figure 8 ShowBorder Designer

Figure 8** ShowBorder Designer **

Since the ShowBorder property is Boolean, it appears as a checkbox in the smart tag panel. In the sample, you'll notice that not only does the border appear and disappear as the ShowBorder designer action property is toggled, but the Properties window's ShowBorder field is also updated immediately to reflect the change. This is the expected result of the actual ShowBorder property's component change service integration.

Multivalue Designer Action Property Items

One other designer action property item that's worth looking at is one that is multivalued, like the HourHand, MinuteHand, and SecondHand properties implemented by ClockControl which, if displayed from the smart tag panel, would look like Figure 9.

Figure 9 Not So Pretty

Figure 9** Not So Pretty **

Unfortunately, the smart tag panel doesn't allow the expandable style of property editing that is supported by the Properties window. At best, the smart tag panel can use the custom Hand class's type converter, HandConverter, which supports conversion between a Hand object and a multivalued string representation of a Hand object. However, as you can see from Figure 9, the string format is not as user friendly as it could be.

Because expandable property editing is not supported from the smart tag panel, you'll need to come up with some alternatives if string editing won't cut it. For example, you might consider providing a separate designer action property item for each constituent property. While this would work, you would need to be conscious of using too much UI real estate, which isn't the best use of the smart tag panel. Alternatively, you could create a new UI type editor specifically for the Hand type, which would consume no more UI real estate than the current solution does now.

Designer Action Method Items

If you consider the physical configuration of the HourHand, MinuteHand, and SecondHand properties to really represent a single, logical unit of configuration, you can change tack and use designer action method items instead. Designer action method items provide single-click access to complex functionality that will likely involve more than a single property set. This functionality would typically be presented via a specialized UI to reduce any complexity. As such, designer action method items are an ideal way to roll the physical configuration of the three hand properties into a single, easy, logical step.

To add a task to the smart tag panel that supports editing all clock hands at once, you would begin by updating GetSortedActionItems in your custom DesignerActionList class, only this time adding a DesignerActionMethodItem object to the DesignerActionItemCollection, as shown here:

public class ClockControlDesignerActionList : DesignerActionList { ... public override DesignerActionItemCollection GetSortedActionItems() { ... // EditClockHands designer action method item actionItems.Add(new DesignerActionMethodItem( this, "EditClockHands", "Edit Clock Hands")); ... } ... }

The DesignerActionMethodItem class's constructor accepts three arguments: a reference to the designer action list object that the designer action method item is associated with, the name of a method implementation that resides in the custom designer action list, and the smart tag panel text for this task. In this case, the method implementation isn't really a proxy method because the smart tag window calls it to do whatever needs to be done. Internally, the method will do all the property reads and writes as necessary to complete the task. With regard to configuring all three clock hands at once, this means grabbing the ClockControl's three hand property values, passing them to a custom form for editing, and writing the property values back to the ClockControl if they were changed. The code that performs these steps is shown in Figure 10.

Figure 10 Configuring All Three Clock Hands

public class ClockControlDesignerActionList : DesignerActionList { ... private void EditClockHands() { // Create form HandsEditorForm form = new HandsEditorForm(); // Set current hand values form.HourHand = _ this.ClockControl.HourHand; form.MinuteHand = _ this.ClockControl.MinuteHand; form.SecondHand = _ this.ClockControl.SecondHand; // Update new hand values if OK button was pressed if( form.ShowDialog() == DialogResult.OK ) { SetProperty("HourHand", form.HourHand); SetProperty("MinuteHand", form.MinuteHand); SetProperty("SecondHand", form.SecondHand); } } ... }

With a DesignerActionMethodItem object and a method implementation in place, a rebuild is required to have the designer action method item appear on the smart tag panel.

As you can tell from Figure 11, designer action method items are displayed as link labels on the smart tag panel. They are also displayed on the Property window's Command panel; if the Command panel is not visible, right-click the Properties window and choose Commands.

Figure 11 Edit Clock Hands Designer Action Method Item

Figure 11** Edit Clock Hands Designer Action Method Item **

You should note that unlike designer action property items, the method implementation of a designer action method item can be set to private, protected, internal, and public. Also, the method must not accept any arguments as the smart tag panel doesn't provide a mechanism to capture and pass them to the designer action method implementation. Conversely, the designer action method implementation must not return a value as the smart tag panel can't receive or process it.

Adding Designer Action Method Items

The sphere of influence exerted by designer action method items extends beyond the smart tag panel. In fact, they can be configured to be displayed in a design-time component's context menu and implicitly in the Properties window. This is achieved through one of the DesignerActionMethodItem class's constructor overloads which accept an additional Boolean argument. When true, the designer action infrastructure adds a menu item to the underlying component's context menu, as well as adding a link label in the Properties window's Description pane. A little tweak to the existing solution is required:

public override DesignerActionItemCollection GetSortedActionItems() { ... // EditClockHands designer action method item actionItems.Add(new DesignerActionMethodItem( this, "EditClockHands", "Edit Clock Hands" , true)); ... }

Note that custom designers implement a Verbs property that you can use to add and update context menus and the Properties window. If you built a custom designer from a version of the .NET Framework earlier than version 2.0 that does use the Verbs property, the Windows Forms designer will automatically turn them into smart tag methods without any effort on your part. Unfortunately, you won't be able to categorize them or lay them out as nicely as native designer action items, which I'll explore shortly.

Toggling the Designer Action Method Item Display Name

One of the common designer action method items you'll find on rectangular controls in Windows Forms 2.0 is the ability to dock and undock those controls to and from their parent container from a designer action method item available from the smart tag panel (this smart tag item can be provided for any control automatically by adding the DockingAttribute to a control). The trick with the Dock/Undock property is to toggle the display name to reflect the current component's Dock state whenever the designer action method item's link label is clicked, ultimately looking like Figure 12.

Figure 12 Dock/Undock Property

Figure 12** Dock/Undock Property **

To begin with, you'll need to create a new designer action method item, with an accompanying method implementation that toggles the Dock property between a DockStyle of Fill and a DockStyle of None, as shown in Figure 13.

Figure 13 Toggling the Dock Property

public class ClockControlDesignerActionList : DesignerActionList { ... public override DesignerActionItemCollection GetSortedActionItems() { ... // Dock/Undock designer action method item actionItems.Add(new DesignerActionMethodItem( this, "ToggleDockStyle", "Dock/Undock in parent container")); ... } ... private void ToggleDockStyle() { // Toggle ClockControl's Dock property if( _clockControl.Dock != DockStyle.Fill ) { SetProperty("Dock", DockStyle.Fill); } else { SetProperty("Dock", DockStyle.None); } } ... }

To toggle the designer action method item's display name, you need two things: a helper method that calculates and returns the appropriate text, and a way to have it called when the dock style changes. With regard to the latter, the smart tag panel is refreshed whenever the DesignerActionService.Refresh method is used, which results in a subsequent call to GetSortedActionItems. Consequently, you can invoke the helper method from the DesignerActionMethodItem's constructor. The updated constructor and new helper method are both shown in Figure 14.

Figure 14 Constructor and Helper Method

public class ClockControlDesignerActionList : DesignerActionList { ... public override DesignerActionItemCollection GetSortedActionItems() { ... // Dock/Undock designer action method item with display name // generated from GetDockStyleText helper method actionItems.Add(new DesignerActionMethodItem( this, "ToggleDockStyle", GetDockStyleText())); ... } ... // Helper method that returns an appropriate display name for the // Dock/Undock property, based on the ClockControl's current Dock // property value private string GetDockStyleText() { if( this.ClockControl.Dock == DockStyle.Fill ) { return "Undock in parent container"; } else { return "Dock in parent container"; } } ... }

Automatically Show the Smart Tag Panel

If you wanted to give developers the option to dock to the parent container or to perform any sort of configuration, as soon as a component is dragged onto a form, you can make use of the DesignerActionList class's AutoShow property. By default, the base implementation of AutoShow will return false, which turns off AutoShow (a user can also disable AutoShow completely through an option in the Tools | Options page). Consequently, you'll need to override it to return:

public class ClockControlDesignerActionList : DesignerActionList { ... public ClockControlDesignerActionList(ClockControl clockControl) : base(clockControl) { // Automatically display smart tag panel when design-time component // is dropped onto the Windows Forms Designer this.AutoShow = true; ... } ... }

Whether your smart tag panel is automatically shown or shown on demand, you'll want it to be a little more approachable. A variety of support for improving the appearance of your smart tag panel is available from the designer action infrastructure.

As you may have noticed, the only order applied to the appearance of the designer action items is that they are listed in the order they are added to the DesignerActionItemCollection in the GetSortedActionItems method. Unfortunately, the default appearance doesn't really provide developers with much additional visual assistance to understand what each designer action item does. Fortunately, you can use several designer action techniques to turn your smart tag panel from drab to fab.

Categories and Descriptions

By default, designer action property and method items are placed into a default category. To help developers distinguish between designer actions and then determine what the specific actions do, you can add the designer actions to custom categories and give them informative text descriptions. Both DesignerActionPropertyItem and DesignerActionMethodItem classes provide constructor overloads that allow you to supply both of these:

public override DesignerActionItemCollection GetSortedActionItems() { ... // Add DigitalTimeFormat designer action property actionItems.Add(new DesignerActionPropertyItem( "DigitalTimeFormat", "Digital Time Format", "Appearance", // Category string argument "The digital time format, ..." // Description string argument )); ... }

When categories are applied, the order in which designer action items are rendered to the smart tag panel is determined first by category and second by the order in which they are added to the designer action item collection. The description is displayed as a tooltip when the mouse hovers over the task.

Usually, custom components like the ClockControl will apply both CategoryAttribute and DescriptionAttribute to their properties to influence their appearance in the Properties window. Consequently, the designer action list class's proxy implementation of these properties should use the same string values. One way to get them is with reflection, as shown in Figure 15.

Figure 15 Using Reflection

public class ClockControlDesignerActionList : DesignerActionList { ... public override DesignerActionItemCollection GetSortedActionItems() { ... // Add DigitalTimeFormat designer action property actionItems.Add(new DesignerActionPropertyItem( "DigitalTimeFormat", "Digital Time Format", GetCategory(this.ClockControl, "DigitalTimeFormat"), GetDescription(this.ClockControl, "DigitalTimeFormat"))); ... } // Helper method to return the Category string from a CategoryAttribute // assigned to a property exposed by the specified object private string GetCategory(object source, string propertyName) { PropertyInfo property = source.GetType().GetProperty(propertyName); CategoryAttribute attribute = (CategoryAttribute) property.GetCustomAttributes(typeof(CategoryAttribute), false)[0]; if( attribute == null ) return null; return attribute.Category; } // Helper method to return the Description string from a // DescriptionAttribute assigned to a property exposed by the specified // object private string GetDescription(object source, string propertyName) { PropertyInfo property = source.GetType().GetProperty(propertyName); DescriptionAttribute attribute = (DescriptionAttribute) property.GetCustomAttributes( typeof(DescriptionAttribute), false)[0]; if( attribute == null ) return null; return attribute.Description; } }

Both GetCategory and GetDescription take two arguments: an object and the name of the property implemented by the object that is adorned with either CategoryAttribute or Description. Then, the TypeDescriptor is employed to retrieve the desired attribute and return the string value. Both GetCategory and GetDescription return null if the source object's property implementation is not adorned with the expected attribute. Null strings passed to the DesignerActionPropertyItem class's constructor are treated as if they weren't provided. The corresponding smart tag task will be placed into the default category and won't have a tooltip. In general, properties that you expose from your components that can be configured from the design time should be adorned with both CategoryAttribute and DescriptionAttribute, especially as they influence the Properties window in the same manner to provide the same benefits.

The reason that both the GetCategory and GetDescription methods expect an object parameter to describe the source object rather than something more specific, like a ClockControl parameter, is to cater to properties located on different types like ClockControlDesigner.ShowBorder. Here's how GetCategory and GetDescription are passed a reference to ClockControlDesigner:

public override DesignerActionItemCollection GetSortedActionItems() { ... // ShowBorder designer action property item actionItems.Add(new DesignerActionPropertyItem( "ShowBorder", "Show Border", GetCategory(this.Designer, "ShowBorder"), GetDescription(this.Designer, "ShowBorder"))); ... }

In general, this technique works well for designer action properties since, as we know, all design-time configurable component or designer properties are likely to be adorned with CategoryAttribute or DescriptionAttribute. Designer action methods, on the other hand, are highly likely to be implemented in their entirety on the custom designer action list, rather than acting as proxies to underlying implementations. Consequently, you'll have to provide category and description strings when instantiating DesignerActionMethodItem, like so:

public override DesignerActionItemCollection GetSortedActionItems() { ... // EditClockHands designer action method item actionItems.Add(new DesignerActionMethodItem( this, "EditClockHands", "Edit Clock Hands","Appearance", "Configure the ClockControl's hour, minute and second hands.", true)); ... }

Fortunately, these are unlikely to be repeated anywhere else. In those few cases where they might be repeated, you should easily be able to refactor both GetCategory and GetDescription methods to operate against methods in addition to properties.

Headers and Labels

If you want categories to be more recognizable, you could assign each one a text header using another designer action item, DesignerActionHeaderItem, whose constructor accepts a single string value, as shown in the following:

public override DesignerActionItemCollection GetSortedActionItems() { ... actionItems.Add(new DesignerActionHeaderItem("Appearance")); // Appearance category designer action items added here ... actionItems.Add(new DesignerActionHeaderItem("Behavior")); // Behavior category designer action items added here ... actionItems.Add(new DesignerActionHeaderItem("Design")); // Design category designer action items added here ... ... }

Even more descriptive would be to provide a piece of descriptive text, which you can do with the DesignerActionTextItem, whose constructor accepts a string category name value plus the text string, as shown here:

public override DesignerActionItemCollection GetSortedActionItems() { ... actionItems.Add(new DesignerActionHeaderItem("Appearance")); actionItems.Add(new DesignerActionTextItem( "Properties that affect how the ClockControl looks.", "Appearance")); // Appearance category designer action items added here ... ... }

The two parameter constructors ensure that labels are sorted first by the category they are assigned to, then in the order in which they are added to the designer action item collection.

Figure 16 ClockControl Smart Tag Pane

Figure 16** ClockControl Smart Tag Pane **

The result of applying categories, descriptions, headers, and labels is the fabulous smart tag panel, shown in Figure 16. You can easily mix and match subsets of these UI elements to support a wide variety of scenarios.

Where Are We?

Your custom components can utilize the .NET Framework 2.0 designer action infrastructure to provide smart tag support. This process is relatively simple, particularly when you need to support more exotic scenarios such as toggling text labels, integrating with the context menu and Properties window, and optimizing smart tag panel usability. Smart tags create a more polished, contemporary, and productive design-time experience for custom components. It is definitely worth your time to learn all about them.

Michael Weinhardt is currently co-updating the book, Windows Forms Programming in C# by Chris Sells and writes a monthly column for MSDN online called Wonders of Windows Forms.