Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Smart Control Designer

0.00/5 (No votes)
1 Dec 2007 1  
The article describes the work principles and ways of using SmartControlDesigner, which supports the custom control design in designtime
Screenshot - BorderSample.png

Introduction

When developing control elements for .NET, our company KBSoft faced a task of providing the design of complex compound control elements in the designtime. Our goal was to provide the convenience of designing of a control element, containing plentiful compound parts (signatures, diagrams, images etc.), which will be displayed on its surface. And for the user's comfort, these elements should be chosen by a mouse in the designtime. So convenience of such a compound control element adjustment should be the same as adjustment of control elements on a form, when a user can choose different control elements, move them and resize with the help of a mouse, adjust features of the chosen control element with the help of Visual Studio property grid.

A standard decision comes first to mind for such a problem statement � implement all parts of a custom control element with the help of other controls. But this approach has a lot of difficulties. It is well-known how difficult it is sometimes to get a demanded behavior from a window. For example, if it is needed to make a text mark, which would have oval borders, on a control element, or to obtain non-transparency text display on a semi-transparent background, there will occur difficulties, which we mentioned in the article Non-transparent controls on a semi-transparent window. Besides, each window takes certain resources, receives messages etc., which is often not needed at all.

If you only need to draw on the surface of your control element with the help of GDI+ or OpenGL and forget about additional windows, placed on the control, but at the same time you want to be free in adjusting your control in the designtime, then it is necessary to use Visual Studio opportunities allowing to add certain logic of visual design. Designers and component model for control elements .NET with the base class Component are used for that. The only disadvantage of this approach is a high complexity of implementation of a good design support in the designtime.

This article contains a ready designer SmartControlDesigner, which we created in KBSoft for internal needs. This designer is able to support adjustment of position of some rectangular areas on the surface of your control element. For such areas the frame is drawn in the style of a standard, drawn when you choose a control element on a form in WindowForms application. This frame allows to adjust the position and area sizes. Such an area can be chosen by a mouse and its properties will be displayed in a property grid. Thus, a user, adjusting your control, has an impression that he is working with the usual control elements on a form. In fact, this area will never be a window and can be a simple object in the memory, drawn by several GDI-functions. In the image below you will see an appearance of an area designed in IDE Visual Studio.

Sample Image - maximum width is 600 pixels

SmartControlDesigner is totally ready to use, and having implemented several interfaces in your element, you receive a perfectly adjustable in the designtime control element. An example of the current designer implementation can be a base for developing of a more flexible and comfortable system of component adjustment in the designtime. SmartControlDesigner source code contains detailed comments and explanations for that.

Background

To manage control element behavior in the designtime, there is a class System.Windows.Forms.Design.ControlDesigner in FCL. It contacts the control element class with the help of an attribute DesignerAttribute in the following way:

[Designer(typeof(KBSoft.Controls.SmartControlDesigner))]
public class UserControl1
{�}

Object-Designer (in this case SmartControlDesigner) is created at the moment of opening a form, where a design control UserControl1 is placed. And the control element, to which the designer is tied, will be accessible through the property of this designer. The property ControlDesigner.Control is intended for that purpose.

For the management of the element's composition in the designtime, it is necessary to have a set of commands (add element, delete element, adjust element properties etc.). The designer allows adding a set of commands, displayed in the designtime for a particular control element. For that, Verbs property should be redefined in the designer. The example is shown below.

public override DesignerVerbCollection Verbs
{
    get
    {
        return this.verbs;
    }
}

Each command is defined by its name and function-handler, which will be activated in response to a click on that command. If we fill a return by the current property collection DesignerVerbCollection with the help of the function verbs.Add(DesignerVerb value), then the command list of the designtime will look as follows:

Sample Image - maximum width is 600 pixels

Various facilities of the designer can be used with the help of several interfaces. IDesignerHost refers to them. It allows to control components, designed in the designtime. Interface ISelectionService allows to react to the user's change of a chosen component in the designtime. And interface IComponentChangeService presents events on deleting or changing of one of the components. References to these interfaces are usually initialized in an overridden function Initialize of Control Designer. This function is called automatically after the designer creation for initialization implementation. GetService(Type serviceType) function, to which the interface type is passed as a parameter is used to get a reference to the interface. Below you will see a fragment of Initialize function from SmartControlDesigner, demonstrating the initialization of necessary interfaces and subscription for mouse events from a control element.

// Get interface IselectionService and save it into the field.

this.selectionService = this.GetService (typeof(ISelectionService))
                        as ISelectionService;

// Get interface IComponentChangeService and save it into the field.

this.componentChangeService = this.GetService (typeof
                        (IComponentChangeService))as IComponentChangeService;

// Get interface IDesignerHost and save it into the field.

this.designerHost = this.GetService (typeof(IDesignerHost))as IDesignerHost;

if (this.selectionService != null)
{
    // Subscription to the event of the chosen element change.

    this.selectionService.SelectionChanged += new EventHandler
                                (SelectionServiceSelectionChanged);
}

if (this.componentChangeService != null)
{
    // Subscription to the event of component deletion.

    this.componentChangeService.ComponentRemoving += new
        ComponentEventHandler (ComponentChangeServiceComponentRemoving);

    // Subscription to the event of change of any property of the component.

    this.componentChangeService.ComponentChanged +=
        new  ComponentChangedEventHandler
        (componentChangeService_ComponentChanged);
}

// Subscription to the mouse events from the control element.

this.Control.MouseDown += new MouseEventHandler (Control_MouseDown);
this.Control.MouseUp += new MouseEventHandler(Control_MouseUp);
this.Control.MouseMove += new MouseEventHandler(Control_MouseMove);
�

In order to control some compound part of a control element in the designtime with the help of a designer, it is necessary that it would represent itself a class, derived from System.ComponentModel.Component. Then it will be possible to add this component into a host (design area IDE Visual Studio, after which edition of its properties with the help of PropertyGrid will be accessible) with the help of a reference to IDesignerHost received earlier in the following way:

this.designerHost.CreateComponent(Type type);

where an object of the Type type, representing itself a reference type, derived from Component will be passed as a function parameter. After adding a component it is displayed in the lower design part of Visual Studio. Its name is formed on the basis of the component type name.

Sample Image - maximum width is 600 pixels

After receipt of the reference to the component with the help of CreateComponent function, this reference is usually passed to the control element, because it "knows" how the created element should be drawn.

After the element creation, the user should have a chance to somehow choose the created component for its properties edition. Component adjustment in the designtime presupposes a support of two choice mechanisms.

Firstly, a user can click on the component icon, displayed in the lower part of the design area (shown in a screenshot above). The component's properties will be displayed in the property grid. In this case, the component (diagram, image, label etc.) drawn in the area should be marked as chosen (for example be in a frame). In order to receive a notification that the user has chosen a component, there is an event ISelectionService.SelectionChanged.

Secondly, the user should have an opportunity to click a mouse in the area of a component, drawn on the control element and choose it. For that, it is necessary to take two actions:

  1. Override the designer function GetHitTest(Point point). This function receives a point, where a click of a mouse was fulfilled. If the function returns true, then it means that the event will be transmitted to the window of the control element (if the designer is subscribed to the messages from control, as it was shown before, it will be able to process them). If the function returns false, then the click will be processed with Visual Studio environment (which usually leads to the choice of the control element as a chosen component).
  2. In the function of processing a mouse click in the designer, determine a component, on which the click was fulfilled and make this component chosen in the IDE Visual Studio environment. The function ISelectionService.SetSelectedComponents(ICollection components) serves that purpose.

Description of Designer Use

  1. Implement the interface IRegionContainer in your designer. This interface allows getting a collection of components, which this control element contains. With the help of this collection, the designer gets to know what components are needed to be managed in the designtime.
  2. Implement the interface IRegion in the components which will be present in your control element. This interface determines a rectangle area of the component.
  3. Associate the designer SmartControlDesigner with the control element with the help of DesignerAttribute attribute.

The interface IRegion contains Bounds property (determines a rectangle area, occupied by the component) and ZOrder (determines an order in which this area will be drawn, that is its remoteness from the observer).

The interface IRegionContainer contains two properties - Regions and VerbNames and two methods - RegionAdded and RegionRemoved. Regions property should return the collection of components (represented by the references to IRegion) which should be controlled by the designer. The designer will automatically draw the frame around the IRegion area; will provide a support of area size changes with the help of a mouse and choice of the component in the designtime when clicking in its area. The IRegion collection of elements returned by this property should also be sorted in correspondence with ZOrder values. The designer will search for an appropriate area from the end to the start of the collection. If you know this fact, you can change the designer behavior when choosing overlapping (placed on one another) areas. For that it is only needed to change the sorting logic.

That is how this feature is implemented in demo-control, which you can find in the demo project.

public RegionList Regions
{
    get
    {
        //  Create a backup collection, containing the resultant regions

        System.Collections.Generic.List<IRegion> regions = new
            System.Collections.Generic.List<IRegion>();

        //  Receive an array of elements ImageItem and copy

        //  these elements to regions.

        ImageItem[] images = new ImageItem[imagesList.Count];

        this.imagesList.CopyTo(images);

        regions.AddRange(images);

        //  Receive an array of elements RectItem and copy

        //  these elements to regions.

        RectItem[] rects = new RectItem[rectList.Count];

        this.rectList.CopyTo(rects);

        regions.AddRange(rects);

        //  Sort regions on ZOrder decrease.

        regions.Sort(delegate(IRegion r1, IRegion r2)
        {
            if (r1.ZOrder == r2.ZOrder)
                return 0;

            if (r1.ZOrder < r2.ZOrder)
                return 1;
            else
                return -1;
        }
        );

        return regions;
    }

    set { }
}

There are two separate collections of components, displayed on the control element � imageList and rectList. The elements of these collections are united into one collection of IRegion links. The components of these two collections can be drawn in the control in any way � it won't affect their behavior in the designtime.

VerbNames property should return the collection of pairs (command_name, type_of_element_created). These commands will be automatically added to the designer with the help of Verbs property. When clicking the command command_name, a component of the type type_of_element_created will be created. After that, the RegionAdded function will be activated, to which the created element will be passed as a parameter. With the help of this function, the control gets to know that a new element was added during the process of designing. For example, this function is implemented in the following way in the demo-application:

public void RegionAdded(IRegion region)
{
    ImageItem ri = region as ImageItem;
    RectItem rect = region as RectItem;

    if (ri != null)
        this.imagesList.Add(ri);
    else
        if (rect != null)
            this.rectList.Add(rect);
}

This function determines which component was created in the designer and adds it to a corresponding collection.

The remaining function RegionRemoved is called when there happens a deletion of a component in the designtime (the user chose a component and clicked Delete). In the demo-project, this function is defined in the following way:

public void RegionRemoved( IRegion region )
{
    ImageItem ri = region as ImageItem;
    RectItem rect = region as RectItem;

    if (ri != null)
          this.imagesList.Remove(ri);
    else
        if (rect != null)
            this.rectList.Remove(rect);
}

Here a reverse procedure is produced � a type of the deleted element is defined and it is deleted from the corresponding collection.

Thus, you can create quite a complicated structure of areas, displayed on the surface of your control element. With the help of Regions feature, you can control the logic of their placement (what control elements are drawn over the rest of the elements).

In conclusion, it is necessary to note that processing of the whole control element deletion should be processed in a special way. If you delete it from the form, its function Dispose will be activated. It is absolutely necessary to activate Dispose functions in the rest of the components added by the designer; otherwise they will remain in the design area, though they won't have a control on which they could be displayed. Dispose activation for the class, derived from System.ComponentModel.Component leads to its deletion from IDE Visual Studio 2005.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here