Creating a Design Surface Extender Framework for Windows Forms 2.0
Debugger Visualizations, Garbage Collection
Use the Visual Studio 2005 Bootstrapper to Kick-Start Your Installation
CLR Inside Out: Improving Application Startup Time
Unit Testing Tips: Write Maintainable Unit Tests That Will Save You Time And Tears
Program Customized Testing Environments Without Trashing Your Machine
Bridge the Gap Between Development and Operations with Whitehorse
Visual Studio 2005: Create Reusable Project And Item Templates For Your Development Team
TOC
Collapse the table of content
Expand the table of content
The document is archived and information here might be outdated

Creating a Design Surface Extender Framework for Windows Forms 2.0

Visual Studio 2005
 

James Galasyn
Microsoft Corporation

August 2006

Applies to:
   Microsoft Visual Studio 2005
   Microsoft .NET Framework 2.0
   Microsoft Windows Forms 2.0

Summary: Learn how to use the new designer types in Windows Forms 2.0 to extend the Windows Forms design surface. (20 printed pages)

Download the code sample in C# and Visual Basic (135 KB) at the Microsoft Download Center.

Contents

Introduction
The DesignSurfaceExtender Architecture
Implementing the DesignSurfaceExtenderLibrary
Putting It All Together
Design Guidelines
Future Work
Conclusion

Introduction

Windows Forms 2.0 provides a powerful new framework for creating custom design-time experiences. These new types enable rapid application development (RAD), which was difficult to achieve in earlier versions. This article shows you how to create a lightweight framework for extending the Visual Studio 2005 design surface in the Windows Forms Designer.

Note   To benefit the most from this article, you should be familiar with the basics of Windows Forms programming and have a passing familiarity with TypeDescriptors and the System.ComponentModel namespace.

Custom Designers

With Visual Studio 2005, you can create the user interface (UI) for Windows applications visually by using the Windows Forms Designer. Whenever you open a component, control, or form with the View Designer command, the Windows Forms Designer attempts to parse the selected file and present its visual representation. This view is called the design surface.

When a component is on the design surface, it may behave differently than it behaves at runtime. For example, the System.Windows.Forms.Timer component doesn't run in design mode. This special behavior is called the design-time behavior. For example, a control that connects to a database at runtime probably should not connect at design time.

You could specify the design-time behavior of the component by writing special-case code in the component itself. This code would look similar to the following code example.

if( base.DesignMode )
{
   // Special design-time behavior. 
}
else
{
   // Run-time behavior. 
}

This solution is suboptimal, because the component's design-time implementation is coupled with the run-time implementation. This means that your component must be revised when you want to change its design-time behavior. In addition, this solution litters your component's code base with distracting designer code.

The prescribed solution is to author a custom designer when you want to provide custom design-time behavior for your component. This isn't as difficult as it sounds, and Windows Forms 2.0 includes several new types that make the job easier than ever.

To implement a custom designer, derive from one of the base classes that implement the IDesigner interface. In conventional usage, you attach your custom designer type to your component by using the DesignerAttribute. Typically, this means that there is one designer type for each component type.

In this article, I take a different approach. With Windows Forms 2.0, you can implement a single designer type to provide design-time behavior to any and all components on the design surface.

Designer Architecture in Windows Forms 2.0

A key aspect of authoring a custom design-time experience is managing the user interface. As a designer developer, you can create your own user interface layers, called adorners.

An adorner is an overlay on the design surface that hosts glyphs. A glyph is a UI element that handles painting and hit-testing. The design surface typically displays several adorner layers, which can be enabled and disabled independently.

User interaction is managed by the new Behavior type. The BehaviorService provides a Behavior stack. The Behavior at the top of the Behavior stack receives mouse and keyboard messages. Several convenient virtual methods are provided for you to control mouse-related behavior, especially drag/drop operations.

This object model differs from that of earlier versions of the .NET Framework, which required you to manage mouse and keyboard interactions in the designer implementation. In that model, much of the UI designer logic is implemented in event handlers. This model required you to write a great deal of supporting logic. In the new model, UI logic is factored into three new classes: the Adorner, the Glyph, and the Behavior types. For more information, see Extending Design-Time Support on MSDN.

The DesignSurfaceExtender Architecture

This article demonstrates how to implement a lightweight, pluggable architecture that provides an adorner management system for the Windows Forms Designer. The DesignSurfaceExtenderLibrary namespace defines three central types:

  • A component, which is visible in the component tray and displays smart tags (DesignSurfaceExtender).
  • A designer, which interacts with the design-time environment (DesignSurfaceExtenderDesigner).
  • Design surface extensions, each of which manages an adorner layer and the glyphs that it hosts (DesignSurfaceExtension-derived classes).

Four extensions are demonstrated: AnchorExtension, MarginExtension, PaddingExtension, and ConvexHullExtension. The AnchorExtension allows you change the Anchor property of a control by double-clicking red square glyphs. The MarginExtension allows you to change the Margin property of a control by dragging a margin dotted-line boundary. The PaddingExtension allows you to change the Padding property of a control by dragging a padding dotted-line boundary. The ConvexHullExtension paints a convex hull around all the controls on the form.

To test the pluggable architecture of the framework, I implemented the ConvexHullExtension in a separate assembly. Figure 1 shows an example of this framework in the Windows Forms Designer.

Click here for larger image

Figure 1. DesignSurfaceExtensionLibrary in action (Click on the image for a larger picture)

Central to this architecture is the concept of a design surface extension, which is modeled by the DesignSurfaceExtension abstract base class. The DesignSurfaceExtension type provides an adorner layer to the design surface and an action list for a smart tag panel. The adorner is enabled and disabled through the Enabled property, and it is repainted when the Invalidate method is called. When an adorner is enabled, it receives mouse and keyboard messages, and its glyphs are painted.

Figure 2 shows the DesignSurfaceExtension abstract base class and three extensions implemented in the main assembly. Most of the common implementation is factored into the DesignSurfaceExtension class, so the child classes are relatively simple.

Aa730843.dsextend02(en-US,VS.80).gif

Figure 2. The DesignSurfaceExtension abstract base class and derived classes

The DesignSurfaceExtender Component

The DesignSurfaceExtender component is the entry point for the custom design-time behavior. To enable the design surface extensions, you drag an instance of the DesignSurfaceExtender from the Toolbox onto your form. The following code listing shows the implementation of the DesignSurfaceExtender component.

[DesignerAttribute( typeof( DesignSurfaceExtenderDesigner ) )]
public partial class DesignSurfaceExtender : Component
{ 
    public DesignSurfaceExtender()
    {
        InitializeComponent()
    }

    public DesignSurfaceExtender( IContainer container )
    {
        container.Add( this );

        InitializeComponent();
    }
}

As you can see, the DesignSurfaceExtender component is completely hollow; it has no implementation of its own. It exists solely to make the design-time system aware of the DesignSurfaceExtenderDesigner class, which enables the custom design surface extensions. The DesignerAttribute associates the component with its designer.

The DesignSurfaceExtender component also provides a visual attachment point for the smart tags that enable and disable the available design surface extensions. Figure 3 shows the smart tag panel that appears when you click the smart tag.

Aa730843.dsextend03(en-US,VS.80).gif

Figure 3. Smart tag panel showing available extensions

The DesignSurfaceExtenderDesigner Class

Design surface extensions are managed by the designer of the DesignSurfaceExtender component, which is called the DesignSurfaceExtenderDesigner. When you place an instance of the DesignSurfaceExtender component on the design surface, its corresponding DesignSurfaceExtenderDesigner is created.

The DesignSurfaceExtenderDesigner class enumerates all the types in the assembly that derive from the DesignSurfaceExtension abstract base class. The designer also finds DesignSurfaceExtension types in other assemblies. The extensions are exposed as the designer's Extensions property.

The designer class also overrides the ActionLists property to provide a DesignerActionListCollection to the DesignSurfaceExtender component's smart tag panel. Figure 4 shows the DesignSurfaceExtenderDesigner class.

Aa730843.dsextend04(en-US,VS.80).gif

Figure 4. The DesignSurfaceExtenderDesigner class

The DesignSurfaceExtenderDesigner class is a custom designer, but instead of providing custom design-time behavior to one component type, it provides custom design-time behavior to all the Windows Forms controls on a form. DesignSurfaceExtenderLibrary accomplishes this without any special metadata or attributes in the controls' source code.

This is a significant departure from the typical usage model for designers, and it is enabled by the new designer types in Windows Forms 2.0.

The DesignSurfaceExtenderDesigner class has very little implementation. Its only function is to collect the available types that derive from the DesignSurfaceExtension abstract base class. The custom appearance and behavior are determined entirely by the extensions.

The extensions are made available by using the Extensions property, which is a ReadOnlyCollection<IDesignSurfaceExtension>. Design surface extensions are discovered by the FindExtensionsInAssembly method.

private List<DesignSurfaceExtension> FindExtensionsInAssembly( 
Assembly a )
{
    List<DesignSurfaceExtension> extensions = 
        new List<DesignSurfaceExtension>();

    Type[] types = a.GetTypes();

    for( int i = 0; i < types.Length; i++ )
    {
        if( types[i].IsSubclassOf( typeof( DesignSurfaceExtension ) ) )
        {
            ConstructorInfo ci = types[i].GetConstructor(
                BindingFlags.NonPublic | BindingFlags.Public | 
                BindingFlags.Instance,
                null,
                new Type[] { typeof( IServiceProvider ) },
                null );

            if( ci != null )
            {
                DesignSurfaceExtension extension = ci.Invoke(
                    new object[] { this.Component.Site } ) as 
                    DesignSurfaceExtension;

                extensions.Add( extension );
            }
        }
    }

    return extensions;
}

The FindExtensionsInAssembly method queries the given assembly for all types that derive from the DesignSurfaceExtension abstract base class. The procedure is a little more verbose than you might expect. Once a design surface extension type is found, it is created by using reflection. I could have used the TypeDescriptor.CreateInstance or the Activator.Create methods, but these only work with types that have public constructors. The demonstration extensions provided in this article are not part of the public API, so their constructors are defined as internal. These can be found with reflection, by specifying the BindingFlags.NonPublic in the Type.GetConstructor method.

Implementing the DesignSurfaceExtenderLibrary

The DesignSurfaceExtenderLibrary exposes several public types that you can use to implement your own extension classes. This section describes the base classes that enable extending the design surface.

The ExtensionGlyph Classes

The appearance and behavior (or "look and feel") of the design surface extensions are implemented by Glyph-derived and Behavior-derived classes. An abstract ExtensionGlyph class provides a default Glyph implementation. Figure 5 shows the ExtensionGlyph classes.

Aa730843.dsextend05(en-US,VS.80).gif

Figure 5. The ExtensionGlyph classes

Glyphs are painted on a corresponding adorner layer. Glyph types commonly have the following relationship with the related adorner type:

  • Several glyphs
  • Several glyphs per control (AnchorGlyph)
  • One glyph per control (MarginGlyph and PaddingGlyph)
  • One glyph (ConvexHullGlyph)

The ExtensionGlyph abstract base class supports all these scenarios.

An extension's appearance is specified by the ExtensionGlyph types on its adorner. The geometry of a glyph usually depends on the state of one or more controls, which means glyphs will often require several designer services to maintain a consistent state. The corresponding DesignSurfaceExtension class provides references to various services in the glyph's constructor.

Each ExtensionGlyph instance monitors changes to controls by subscribing to the IComponentChangeService.ComponentChanged event. Each ExtensionGlyph type specifies the properties that affect its appearance by overriding the PopulateDependentProperties method. When a ComponentChanged event is raised, each ExtensionGlyph specifies how it responds by overriding the ComponentChangedCallback method, which is intended for use by extensions that provide glyphs to each control on the design surface. Glyphs that use different semantics—for example, a single glyph for the entire design surface—can override the ComponentChangedEventHandler for complete control when controls change.

ExtensionsGlyph classes specify their appearance by overriding the Paint method, and they specify their geometry by overriding the ComputeGeometry method. If a glyph has mouse interaction, it overrides the GetHitTest method to inform the design environment when the mouse is over the active region of the glyph.

The MarginGlyph and PaddingGlyph classes have nearly identical appearance and behavior, so their implementation is factored into the BoundsGlyph and BoundsBehavior abstract base classes. Figure 6 shows details of the ExtensionGlyph classes.

Click here for larger image

Figure 6. Details of the ExtensionGlyph classes (Click on the image for a larger picture)

The Behavior Classes

The Behavior classes vary widely in complexity. The most simple will do nothing with the mouse and keyboard messages they receive. Others, such as BoundsBehavior, enable more complex gestures like drag/drop. Figure 7 shows the Behavior classes.

Aa730843.dsextend07(en-US,VS.80).gif

Figure 7. The Behavior classes

The Windows Forms 2.0 designer architecture factors keyboard and mouse handling into the Behavior class. The BoundsBehavior is the most interesting behavior class in the DesignSurfaceExtensionLibrary, because it implements drag/drop functionality.

The MarginExtension and PaddingExtension classes enable visually assigning the Margin and Padding properties for any control on the design surface. The user clicks on the MarginGlyph or PaddingGlyph and drags the selected edge to set the property value.

Implementing drag/drop behavior in the Windows Forms 2.0 design environment is not entirely straightforward. Most important for correct drag/drop operation is proper use of an enclosing DesignerTransaction, which enables undo/redo operations.

This default implementation is useful enough that it might be desirable to factor it into a separate DragDropBehavior base class. Figure 8 shows details of the Behavior classes.

Click here for larger image

Figure 8. Details of the Behavior classes (Click on the image for a larger picture)

The DesignerActionList Classes

The DesignerActionList class is new to Windows Forms 2.0. You derive your own action list type to specify the items in a smart tag panel. The user enables and disables the design surface extensions by using the smart tag panel. The DesignSurfaceExtensionLibrary departs from the typical usage model by connecting the smart tag panel to DesignSurfaceExtension instances instead of the panel's attached component. When the user clicks on a check box in the smart tag panel, the corresponding DesignSurfaceExtension.Enabled property is set.

The DesignSurfaceExtenderDesigner provides its own default smart tag items, "Enable All Extensions" and "Disable All Extensions." Figure 9 shows the ExtensionActionList classes.

Aa730843.dsextend09(en-US,VS.80).gif

Figure 9. The ExtensionActionList classes

Putting It All Together

If my framework design is successful, using the DesignSurfaceExtensionLibrary types should be intuitive and effective. This section describes how you can use the framework to create your own extensions.

The Anchor, Margin, Padding, and Convex Hull Extensions

Three design surface extensions are implemented in the DesignSurfaceExtensionLibrary: AnchorExtension, MarginExtension, and PaddingExtension. All derive from the DesignSurfaceExtension class. Each design surface extension provides a DesignerActionList and an adorner that is pre-populated with glyphs.

The ActionList property is used to populate the smart tag panel attached to the DesignSurfaceExtender component. The DesignerActionList has a single DesignerActionPropertyItem, which enables or disables the corresponding extension. The ExtensionActionListBase class provides a default implementation.

Internally, each extension class provides an adorner that is populated with ExtensionGlyph objects. Each ExtensionGlyph is bound to a corresponding Behavior object. The Glyph and the Behavior types together implement the specific appearance and behavior encapsulated by the extension.

One of the functions of the DesignSurfaceExtension class is to keep the adorner's Glyphs collection synchronized with the controls on the design surface. This means that when controls are added or removed, the extension updates its state accordingly. The DesignSurfaceExtension class accomplishes this by handling the ComponentAdded and ComponentRemoved events provided by the IComponentChangeService. By default, when a control is added or removed, the adorner's Glyphs collection is cleared and repopulated.

Handling these events reveals a peculiar feature of the IComponentChangeService. When the ComponentAdded and ComponentRemoved event is raised, the IReferenceService has not yet updated its collection of references. The unfortunate result is that calls to IReferenceService.GetReferences return a stale object array, with the added control not present or the removed control still present.

The default implementation of ComponentAddedEventHandler provides a workaround by explicitly calling AddGlyphsForControl for the newly added control, which is provided by the ComponentEventArgs.Component property.

protected virtual void ComponentAddedEventHandler( object sender, 
ComponentEventArgs e )
{
    if( e.Component is Control )
    {
        if( this.Enabled )
        {
            this.PopulateAdorner( this.adornerValue );

            // IReferenceService workaround.
            this.AddGlyphsForControl( e.Component as Control );
        }
    }
}

This workaround may not be appropriate for other implementations, so the ComponentAddedEventHandler and ComponentRemovedEventHandler are protected virtual methods. Note that the framework design guidelines recommend against exposing event handlers as public members. Fortunately, design-time code only runs in full-trust, so partially trusted callers can't attach arbitrary event handlers at these points. Figure 10 shows details of the DesignSurfaceExtension classes.

Click here for larger image

Figure 10. Details of DesignSurfaceExtension classes (Click on the image for a larger picture)

To further test my framework, I implemented an extension named ConvexHullExtension. Its adorner has a single ConvexHullGlyph instance, which paints the convex hull of all the controls on the form. The convex hull of a set of points S is the smallest convex set that contains S. Essentially, the convex hull contains all the outermost corners of all the controls on the design surface. Imagine stretching a rubber band around all the controls. The convex hull is the polygon formed when the rubber band snaps into place around the controls. I adapted a C++ implementation of the convex hull algorithm by Carlos Moreno, first published in C/C++ User's Journal.

Creating Your Own Design Surface Extensions

To create a design surface extension with the DesignSurfaceExtensionLibrary types, follow these steps.

  1. Derive your DesignerActionList class from ExtensionActionListBase. In the constructor, assign the name of the extension to the displayTextValue field.
    namespace ConvexHullDesignSurfaceExtension
    {
        internal class ConvexHullActionList : ExtensionActionList
        {
            internal ConvexHullActionList( DesignSurfaceExtension extension )
                : base( extension )
            {
                this.displayTextValue = "Convex Hull";
            }
        }
    }
    
  2. Derive your extension class from DesignSurfaceExtension. Override the AddGlyphsForControl and RemoveGlyphsForControl methods. Optionally, you may override the PopulateAdorner method if your custom extension has a behavior other than providing glyphs to each control on the form.
    protected override void PopulateAdorner( Adorner adorner )
    {
        adorner.Glyphs.Clear();
    
        hullGlyph = new ConvexHullGlyph(
            this.BehaviorService,
            this.ComponentChangeService,
            this.SelectionService,
            this.ReferenceService,
            this.ServiceProvider,
            adorner );
    
        adorner.Glyphs.Add( hullGlyph );
    }
    
  3. Override the ActionList property to return your DesignerActionList implementation.
    public override DesignerActionList ActionList
    {
        get
        {
            if( this.actionListValue == null )
            {
                this.actionListValue = new ConvexHullActionList( this );
            }
    
            return this.actionListValue;
        }
    }
    
  4. Derive your Glyph class from the ExtensionGlyph class.
  5. Override the ComputeGeometry method to specify the bounds of your glyph. Typically, this value depends on the geometry of the related control. The BehaviorService.ControlRectInAdornerWindow method is helpful for translating the control's bounding rectangle into the adorner's coordinate frame.
    protected override void ComputeGeometry()
    {
        List<Point> points = new List<Point>();
    
        object[] controls = base.referenceService.GetReferences( 
            typeof( Control ) );
    
        for( int i = 0; i < controls.Length; i++ )
        {
            Control c = controls[i] as Control;
    
            if( !( c is Form ) )
            {
                Point p = this.behaviorService.ControlToAdornerWindow( c );
    
                Point upperLeft = p;
                Point upperRight = new Point( p.X + c.Width, p.Y );
                Point lowerLeft = new Point( p.X, p.Y + c.Height );
                Point lowerRight = new Point( p.X + c.Width, p.Y + c.Height );
    
                points.Add( upperLeft );
                points.Add( upperRight );
                points.Add( lowerLeft );
                points.Add( lowerRight );
            }
        }
    
        List<Point> convexHullPoints = ComputeConvexHull( points );
    
        this.convexHullRegion = ConvertPointsToRegion( convexHullPoints );
    }
    
  6. Override the PopulateDependentProperties method and return a ReadOnlyCollection<string> containing all the Control members on which your Glyph class depends.
    Protected override ReadOnlyCollection<string> 
    PopulateDependentProperties()
    {
        List<string> properties = new List<string>();
    
        properties.Add( "Size" );
        properties.Add( "Height" );
        properties.Add( "Width" );
        properties.Add( "Location" );
    
        return new ReadOnlyCollection<string>( properties );
    }
    
  7. When one of these properties on the related control changes its value, the default behavior in the ExtensionGlyph class is to re-compute the geometry of the glyph and invalidate the adorner. Override the ComponentChangedEventHandler method to change this behavior.

    Unlike the other ExtensionGlyph classes, the ConvexHullGlyph's geometry depends on every control on the design surface, so I had to override the ComponentChangedEventHandler method.

    protected override void ComponentChangedEventHandler( object sender, 
    ComponentChangedEventArgs e )
    {
        if( e.Component is Control )
        {
            string propertyName = e.Member.Name;
    
            if( this.DependentProperties.Contains( propertyName ) )
            {
                this.ComputeGeometry();
    
                if( this.adorner.Enabled )
                {
                    this.adorner.Invalidate();
                }
            }
        }
    }
    
  8. Implement the Behavior class. Custom Behavior implementations can vary widely in complexity. For example, the AnchorBehavior simply overrides the OnMouseDoubleClick method to enable or disable an Anchor setting when the AnchorGlyph is double-clicked. The BoundsBehavior class, on the other hand, implements drag/drop logic, which is relatively involved.

    The ConvexHullExtension has no mouse interaction, so its associated Behavior class is essentially hollow.

    namespace ConvexHullDesignSurfaceExtension
    {
        public class ConvexHullBehavior : Behavior
        {
            public override Cursor Cursor
            {
                get
                {
                    return base.Cursor;
                }
            }
        }
    }
    

Plug-in Assemblies

The DesignSurfaceExtenderLibrary is intended to realize a simple, pluggable architecture. Third-party assemblies are placed in a known location, typically a "Plugin" directory, and the DesignSurfaceExtenderLibrary scans these assemblies for usable extension types.

Currently, the DesignSurfaceExtenderDesigner class is not very smart about finding other assemblies that contain DesignSurfaceExtension types. It has a hard-coded path to a Plugins directory. The ConvexHullExtension is deployed in its own assembly. The ConvexHullExtension project dumps its built binaries (and dependencies) into the Plugins directory. At the very least, this path should be specified by configuration, instead of in code.

private static string pluginPath = 
@"C:\Projects\Designer\DesignSurfaceExtenderLibrary\DesignSurfaceExtenderLibrary\Plugins";

Plug-in assemblies must have a corresponding version of the DesignSurfaceExtenderLibrary assembly in the same directory; otherwise the Assembly.GetTypes call will fail, because it can't resolve dependencies. In a production environment, the DesignSurfaceExtenderLibrary assembly would be signed and installed to the global assembly cache (GAC). This would prevent type resolution failures and allow plugin assemblies to reside in arbitrary (but fully trusted) locations.

Type resolution is also problematic. For example, the FindExtensionsInAssembly method fails to find DesignSurfaceExtension types when the DesignSurfaceExtenderLibrary assembly is accessed from out-of-process. The FindAvailableExtensions method instead uses the Assembly.GetExecutingAssembly method to find the internal extension types (for example, AnchorExtension). The ITypeResolutionService may be helpful in addressing this issue.

Design Guidelines

Wherever possible, the object model presented in this article follows the guidelines specified in Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries, by Krzysztof Cwalina and Brad Abrams, published by Addison-Wesley, 2005.

One of the overarching guidelines employed in this article is to minimize the surface area of the public API. For example, the concrete extension types, AnchorExtension, MarginExtension, and PaddingExtension, are all declared to be internal. This means they are not intended for use outside the DesignSurfaceExtenderLibrary assembly. Instead, the extensibility point is the abstract DesignSurfaceExtension class, which is intended to be used by third-party developers.

Sometimes, the underlying framework defines member access, and implementations in the DesignSurfaceExtenderLibrary have no choice but to keep the ball rolling. For example, the DesignSurfaceExtenderDesigner class inherits from the ComponentDesigner class and exposes the public ActionLists property. My preference would be to restrict the public API for DesignSurfaceExtenderDesigner and mark ActionLists as private or internal, but the base class specifies public access for this property.

I considered making DesignSurfaceExtenderDesigner sealed, but the guidelines say that this should only be done if there is a specific reason. Marking the DesignSurfaceExtenderDesigner type as internal adequately communicates (and enforces) that this is not an extensibility point.

Earlier versions of my DesignSurfaceExtenderLibrary design used static factory methods to create various instances. Although this has been my preference in the past, the design guidelines recommend favoring constructors, except in certain situations. None of those situations clearly applied to my object model, so I decided on implementing internal constructors.

For most of the initial development, I modeled the design surface extender abstraction as an interface called IDesignSurfaceExtension. To separate contract from implementation, the guidelines recommend preferring abstract classes over interfaces. In general, abstract base classes version much better, and they communicate the presence of extensibility points as clearly. On the other hand, Brian Pepin notes "another sign that you've got a well-defined interface is that the interface does exactly one thing." (p. 83). In the case of IDesignSurfaceExtension, the extension abstraction was succinctly represented and was unlikely to require revisions in the future, so I continued to model it as an interface.

It was the implementation of the FindExtensionsInAssembly method that finally changed my mind. Although the Type class provides numerous convenience properties and methods, none of them enables an "implements interface" feature. For example, the Type.IsSubclassOf method does not return true for any of the classes that implemented IDesignSurfaceExtension. This means the FindExtensionsInAssembly method had to rummage through all the interfaces on a type until it found the IDesignSurfaceExtension interface. Because using IsSubclassOf is so much clearer, I was persuaded to abandon the interface model.

To determine whether the DesignSurfaceExtenderLibrary is usable to third-party developers, I implemented a simple extension in a separate assembly. The ConvexHullExtension consumes the DesignSurfaceExtenderLibrary types and uses them in a somewhat different way. For the framework to be useful, I found it necessary to relax some of the access modifiers and make them protected instead of private or internal. As a general rule, it works well to lock up the public API as much as possible, then selectively open it where necessary, as determined by specific scenarios.

The design surface extension is the only new abstraction introduced by the DesignSurfaceExtenderLibrary, so all the other types in my library derive from existing .NET Framework types. Abstract base implementations are provided where appropriate. For example, the ExtensionGlyph class provides the expected behavior of a Glyph class in the DesignSurfaceExtenderLibrary.

Future Work

The DesignSurfaceExtenderLibrary is very much a v1 effort. Several areas of improvement suggest themselves.

Configuration

Separating configuration from implementation is as important as separating interface from implementation. As noted earlier, the plug-in model needs to be driven by configuration.

Other elements should also be specified by configuration, for example hit-test distances and glyph colors.

It might be worthwhile to investigate the utility of integrating with the Enterprise Library and its configuration model.

Memory Churn

The DesignSurfaceExtenderLibrary creates and destroys a lot of Glyph objects, which means memory churn. This can lead to performance issues, especially with painting performance.

It might be worthwhile to implement singleton instances of ExtensionGlyph. This approach would require working around the underlying .NET Framework model, but this shouldn't disrupt my object model too much.

Implementing singleton semantics would also enable configuration-driven object creation. This approach would integrate well with the Enterprise Library's object creation model.

Checking Z-Order

ExtensionGlyph types are drawn even when controls overlap. It may be worthwhile to query controls for their z-order and paint or hide glyphs accordingly.

Drag Feedback

Extensions that support mouse drag behavior could benefit by providing drag feedback. This could be as simple as adding a bit of painting code in the BoundsBehavior.OnMouseMove method.

Control Selection

The extensions shown in this article ignore selection state. Controls are provided with extensions regardless of which controls are selected. The Behavior-derived types could take the primary and secondary selections into account and change property values accordingly. For example, MarginBehavior could change the Margin property for all selected controls at once when a drag gesture occurs.

Designer State

In the current implementation, the state of the extensions is not persisted. This means that Visual Studio 2005 has no memory of an extension's state when the Windows Forms Designer most recently exited. Each time you open your form in the designer, the extensions are disabled. It may be desirable to serialize this state when the other components on the design surface are serialized.

In addition, configuration could drive the initial state of the extensions when the designer is opened.

Enforce Singleton Behavior for the DesignSurfaceExtender Component

In the current implementation, the user can place many instances of the DesignSurfaceExtender component into the component tray. This is undesirable and would almost certainly lead to confusing and possibly nondeterministic behavior. It would be desirable to create a ToolboxItem implementation or some other mechanism to limit the number of DesignSurfaceExtender component instances to one.

Deploy the DesignSurfaceExtenderLibrary as a VSPackage

The DesignSurfaceExtenderLibrary could be deployed as a VSPackage. This solution would eliminate the need for the DesignSurfaceExtender component and its corresponding designer, and it would resolve the singleton issue.

Conclusion

The Windows Forms 2.0 designer types make it easier than ever to create an advanced design-time look and feel for your components and controls. For more information on Windows Forms, check out:

Show:
© 2016 Microsoft