Smart Control Designer

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 convenience of designing of a control element, containing plentiful compound parts (signatures, diagrams, images, and so forth) that will be displayed on its surface. And, for the user's comfort, these elements should be chosen by a mouse in the designtime. So, the convenience of such a compound control element adjustment should be the same as of 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, and adjust features of the chosen control element with the help of VisualStudio property grid.

A standard decision comes first to mind for such a problem statement—mplement all parts of a custom control element with the help of other controls. But, this approach is difficult. It is well known how difficult it sometimes is to get a demanded behavior from a window. For example, if you need to make a text mark that would have oval borders, on a control element, or to obtain non-transparent text display on a semi-transparent background, difficulties will occur; these difficulties were mentioned in the article Non-transparent controls on a semi-transparent window. Besides, each window takes certain resources, receives messages, and so on, that often are not needed at all.

If you need to draw only 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, you must use VisualStudio opportunities that allow you to add certain visual design logic. Designers and a component model for control elements in .NET with the base class Component are used for that. The only disadvantage of this approach is a high complexity of implementation and good design support in the designtime.

This article contains a ready designer SmartControlDesigner, that we created in KBSoft for internal needs. This designer is able to support the adjustment of the 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 you 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 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. On the following image, you will see an appearance of an area designed in the VisualStudio IDE.

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

Smart Control Designer

Background

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

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

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

To manage an element's composition in the designtime, it is necessary to have a set of commands (add element, delete element, adjust element properties, and the like). The designer allows you to add a set of commands, displayed in the designtime for a particular control element. For that, the 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 of a click on that command. If you want to fill a returned value with the current DesignerVerbCollection property collection with the help of the verbs.Add(DesignerVerb value) function, the command list of the designtime will look like this:

[VerbsSample.png]

Various facilities of the designer can be used with the help of several interfaces. IDesignerHost refers to them. It allows you to control components, designed in the designtime. The ISelectionService interface allows you to react at the user's change of a chosen component in the designtime. And, the IComponentChangeService interface 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. The 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 see a fragment of the 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);
...

Smart Control Designer

To control some compound part of a control element in the designtime with the help of a designer, it is necessary to represent itself as a class, derived from System.ComponentModel.Component. Then, it will be possible to add this component into a host (design area IDE VisualStudio, after which edition of its properties with the help of PropertiGrid 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.

[ComponentSample.png]

After receipt of the reference to the component with the help of CreateComponent function, this reference usually is 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 properites edition. Component adjustment in the designtime presupposes a support of two choice mechanisms.

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

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

  1. Override the designer function GetHitTest(Point point). This function receives a point where a mouse click was fulfilled. If the function returns true, it means that the event will be transmitted to the widow 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, the click will be processed with the VisualStudio 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 VisualStudio environment. The ISelectionService.SetSelectedComponents(ICollection components) function serves that purpose.

Description of Designer Use

  1. Implement the interface IRegionContainer in your designer. This interface allows you to get a collection of components, which this control element contains. With the help of this collection, the designer gets to know what components must be managed in the the designtime.
  2. Implement the IRegion interface in the components that 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 IRegion interface contains a 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 IRegionContainer interface contains two properties, Regions and VerbNames, and two methods, RegionAdded and RegionRemoved. The 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 and provide support of the 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, you only need to change the sorting logic.

That is how this feature is implemented in demo-control, which you can find in 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 discrease.
      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 { }
}

Smart Control Designer

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.

The VerbNames property should return the collection of pairs (command_name, type_of_element_created). These commands will be added automatically to the designer with the help of the 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 value to its collection.

The remaining RegionRemoved function is called a deletion of a component in the designtime (the user chose a component and clicked Delete) happens. 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 the Regions feature, you can control the logic of their placement (what control elements are drawn over the rest elements).

In conclusion, it is necessary to note that the 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 all 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 the VisualStudio 2005 IDE.



About the Author

Anton Zlobin

Anton Zlobin is a .NET developer at KB_Soft Group, an offshore software development company located in Russia, Novosibirsk. Here he has worked on various .NET projects. He has a Master Degree in Computer Technology from Novosibirsk State Technical University.

Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • "Security" is the number one issue holding business leaders back from the cloud. But does the reality match the perception? Keeping data close to home, on premises, makes business and IT leaders feel inherently more secure. But the truth is, cloud solutions can offer companies real, tangible security advantages. Before you assume that on-site is the only way to keep data safe, it's worth taking a comprehensive approach to evaluating risks. Doing so can lead to big benefits.

  • Java developers know that testing code changes can be a huge pain, and waiting for an application to redeploy after a code fix can take an eternity. Wouldn't it be great if you could see your code changes immediately, fine-tune, debug, explore and deploy code without waiting for ages? In this white paper, find out how that's possible with a Java plugin that drastically changes the way you develop, test and run Java applications. Discover the advantages of this plugin, and the changes you can expect to see …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds