Click here to Skip to main content
15,881,380 members
Articles / Programming Languages / C#

Rich Control Designer Templates

Rate me:
Please Sign up or sign in to vote.
4.20/5 (3 votes)
14 Jul 2015CPOL5 min read 7K   5   3  
C# template classes to simplify adding rich design-time experiences to custom components and controls. Includes verbs, action lists, glyphs, behaviors and editor window interaction.

Introduction

In authoring custom controls and components under .NET for use in the Visual Studio designer, developers can add rich design-time experiences that simplify and enhance the consumer experience. In developing controls myself, I have found this process difficult and error prone. To improve my own experience, I have developed a set of classes, template classes and extension methods that I will represent here. All these classes help to automate the work done when using ComponentDesigner, ControlDesigner, ParentControlDesigner and DesignerActionList.

Using the code

In the included GenericDesigner.cs source code, there are a number of classes that are useful in developing a custom designer. I will demonstrate the use of each class using a sample designer.

Class Declaration

Declaring the designer class uses one of three template classes, depending on if your component is a component (RichComponentDesigner), control (RichControlDesigner) or parent control (RichParentControlDesigner). Each of these classes takes the type of the component or control and the type of the corresponding DesignerActionList. The designer class must supply an empty constructor and the action list class must supply a constructor that takes instances of the two types defined by the generic class as parameters.

So, your minimal implementation would look something like the following:

C#
internal class ControlListBaseDesigner :
   RichControlDesigner<ControlListBase, ControlListBaseDesigner.ActionList>
{
   public ControlListBaseDesigner()
   {
   }

   public class ActionList :
      RichDesignerActionList<ControlListBaseDesigner, ControlListBase>
   {
      public ActionList(ControlListBaseDesigner d, ControlListBase c) : base(d, c)
      {
      }
   }
}

If you've done work with designers before, you'll remember building a bunch of scaffolding that is loosely coupled to your code for adding verbs and actions. This is quite error prone as if you change the scaffolding, you may break the linkage to internal properties and methods. This solution employs attributes to identify verb and action handlers.

Property Redirection

There are times when you may wish to handle the values of a component's properties differently when within the designer. The method to do this involves duplicating the component's property with your designer class and decorating it with RedirectedDesignerProperty. At design-time, when the designer wants to get or set the value for this property, it will redirect to the code in your designer.

In this example, the ForeColor item in the component's property grid will always display Red, regardless of its actual value. You'll notice that the property has other attributes. These attributes will also override those of the component.

C#
[RedirectedDesignerProperty]
[DefaultValue(typeof(Color), "0xFF0000")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
private Color ForeColor
{
   get { return Color.Red; }
   set { this.Control.ForeColor = value; }
}

Designer Verbs

To add a verb, you simply need to include an EventHandler delegate method decorated with a DesignerVerb attribute to your top-level designer class.

C#
[DesignerVerb("Dock in Parent")]
private void DockInParent(object sender, EventArgs e)
{
   this.Control.Dock = DockStyle.Fill;
}

Designer Actions

There are a few different types of actions: methods, properties, and headers. With this solution, headers are free once you define methods and properties. All supporting attributes allow for the display name, order, category, description and conditions. These methods and properties, unlike those for verbs, are included in the internal action list class derived from RichDesignerActionList.

  • Display Name: Included as the first mandatory parameter for each attribute
  • Order: Included as a second optional parameter for each attribute. If left as '0', then actions are ordered by sorted display name.
  • Category: (Optional) This is a grouping mechanism and will automatically create header items.
  • Description: (Optional) This text is displayed as a tooltip for the item.
  • Condition: (Optional) This is the name of a get property of type 'Boolean' that is called to determine if the action should be included when the user clicks the action arrow. Warning: I had to violate one of my design principles here as attribute properties are limited to a few base types and delegate references are not one of them! Be careful and make sure your string value here actually points to a boolean property.

Designer Action Properties

To add an action property, that shows up using the UITypeEditor for the property type in the action menu, you add a get/set property with a DesignerActionProperty attribute.

C#
[DesignerActionProperty("Dock", 1, Category = "Layout", Description = "Defines which borders of the control are bound to the container.")]
public DockStyle Dock
{
   get { return this.Component.Dock; }
   set { if (this.Component.Dock != value) { this.Component.Dock = value; } }
}

This example will show a drop-down with the DockStyle enumerated values under a "Layout" heading. You'll notice that internally, it uses the Component property to access the Component defined by the generic type in the class declaration.

Designer Action Methods

To add an action method, that shows up as a link in the action menu, you add a parameterless, void method decorated with a DesignerActionMethod attribute.

C#
[DesignerActionMethod("Edit Items...", 0, IncludeAsDesignerVerb = true, Condition = "CanEdit")]
public void EditItems()
{
   EditorServiceContext.EditValue(this.ParentDesigner, this.Component, "Items");
}

internal bool CanEdit
{
   get { return this.Component.Editable; }
}

You'll notice the use of the "Condition" option that will use the declared "CanEdit" property to determine if the "Edit Items..." link should be shown on the action list. This can be used dynamically as it is called each time the action list is shown. This attribute also uses a special option, "IncludeAsDesignerVerb", that when set to 'true' will add the display text to the list of verbs.

Glyphs and Behaviors

Glyphs represent those items drawn on the designer surface and Behaviors represent user interactions with the surface. All glyphs and behaviors derive from RichGlyph and RichBehavior respectively and each templated class takes a ComponentDesigner derived class type. In reality, these don't do much. They simply expose properties for the designer. If you don't need to interact with the design surface, you can omit the implementation of these classes.

To add your glyph to the designer, simply add the following line to your designer's constructor adding the glyph or glyphs you wish to use:

C#
this.Glyphs.Add(new ControlListBaseDesignerGlyph(this));

The implementation here merely shows a simple example of how to watch for mouse movement and paint on the surface. This is almost the same amount of work you'd have to do without the helper classes.

C#
internal class ControlListBaseDesignerBehavior : RichBehavior<ControlListBaseDesigner>
{
   public ControlListBaseDesignerBehavior(ControlListBaseDesigner designer)
      : base(designer)
   {

   }

   public override bool OnMouseDown(System.Windows.Forms.Design.Behavior.Glyph g, MouseButtons button, Point mouseLoc)
   {
      if (button == MouseButtons.Left)
      {
         switch (((ControlListBaseDesignerGlyph)g).LastHit)
         {
            case ControlListBaseDesignerGlyph.ClickState.FirstBtn:
               this.Designer.Control.Owner.SelectedItem = this.Designer.Control.Owner[0];
               break;
            case ControlListBaseDesignerGlyph.ClickState.LastBtn:
               this.Designer.Control.Owner.SelectedItem = this.Designer.Control.Owner[this.Designer.Control.Owner.Items.Count - 1];
               break;
            default:
               break;
         }
      }
      return base.OnMouseDown(g, button, mouseLoc);
   }
}

internal class ControlListBaseDesignerGlyph : RichGlyph<ControlListBaseDesigner>
{
   private const int btnCount = 2, btnSize = 16, navBoxWidth = (btnSize * btnCount) + ((btnCount - 1) * 2) + 4, navBoxHeight = btnSize + 4;
   private Rectangle navBox;

   public ControlListBaseDesignerGlyph(ControlListBaseDesigner designer)
      : base(designer, new ControlListBaseDesignerBehavior(designer))
   {
      base.Designer.SelectionService.SelectionChanged += selSvc_SelectionChanged;
      base.Designer.Control.Move += control_Move;
      base.Designer.Control.Resize += control_Move;
   }

   internal enum ClickState
   {
      Control, FirstBtn, LastBtn
   }

   public override Rectangle Bounds
   {
      get { return navBox; }
   }

   internal ClickState LastHit { get; set; }

   public override void Dispose()
   {
      base.Designer.SelectionService.SelectionChanged -= selSvc_SelectionChanged;
      base.Designer.Control.Move -= control_Move;
      base.Designer.Control.Resize -= control_Move;
      base.Dispose();
   }

   public override Cursor GetHitTest(Point p)
   {
      Rectangle r1 = new Rectangle(navBox.X + 2, navBox.Y + 2, btnSize, btnSize);
      for (int i = 0; i < btnCount; i++)
      {
         if (r1.Contains(p))
         {
            LastHit = (ClickState)(i + 1);
            return Cursors.Arrow;
         }
         r1.Offset(btnSize + 2, 0);
      }
      LastHit = ClickState.Control;
      return null;
   }

   public override void Paint(PaintEventArgs pe)
   {
      bool isMin7 = (Environment.OSVersion.Version >= new Version(6, 1));
      string fn = isMin7 ? "Webdings" : "Arial Narrow";
      string[] btnText = isMin7 ? new string[btnCount] { "9", ":" } : new string[btnCount] { "«", "»" };
      using (Font f = new Font(fn, btnSize - 2, isMin7 ? FontStyle.Regular : FontStyle.Bold, GraphicsUnit.Pixel))
      {
         pe.Graphics.FillRectangle(SystemBrushes.Control, new Rectangle(navBox.X, navBox.Y, navBox.Width + 1, navBox.Height + 1));
         using (var pen = new Pen(SystemBrushes.ControlDark, 1f) { DashStyle = System.Drawing.Drawing2D.DashStyle.Dot })
         {
            pe.Graphics.DrawRectangle(pen, navBox);
            Rectangle r1 = new Rectangle(navBox.X + 2, navBox.Y + 2, btnSize, btnSize);
            pen.DashStyle = System.Drawing.Drawing2D.DashStyle.Solid;
            StringFormat sf = new StringFormat() { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
            for (int i = 0; i < btnCount; i++)
            {
               pe.Graphics.DrawRectangle(pen, r1);
               r1.Offset(1, 1);
               pe.Graphics.DrawString(btnText[i], f, SystemBrushes.ControlDark, r1, sf);
               r1.Offset(btnSize + 1, -1);
            }
         }
      }
   }

   private void control_Move(object sender, EventArgs e)
   {
      if (object.ReferenceEquals(base.Designer.SelectionService.PrimarySelection, base.Designer.Control))
      {
         this.SetNavBoxes();
         base.Designer.Adorner.Invalidate();
      }
   }

   private void selSvc_SelectionChanged(object sender, EventArgs e)
   {
      if (object.ReferenceEquals(base.Designer.SelectionService.PrimarySelection, base.Designer.Control))
      {
         this.SetNavBoxes();
         base.Designer.Adorner.Enabled = true;
         base.Designer.Control.Owner.DesignerSelected = true;
      }
      else if (base.Designer.Control.Owner.DesignerSelected)
      {
         base.Designer.Adorner.Enabled = false;
         base.Designer.Control.Owner.DesignerSelected = false;
      }
   }

   private void SetNavBoxes()
   {
      var pt = base.Designer.BehaviorService.ControlToAdornerWindow(base.Designer.Control);
      navBox = new Rectangle(pt.X + base.Designer.Control.Width - navBoxWidth - 17, pt.Y - navBoxHeight - 5, navBoxWidth, navBoxHeight);
   }
}

Miscellaneous Goodies

Removing Component or Control Properties

If you wish to easily remove properties from the designer without going through the effort of redeclaring each of them in your component and then changing the Browsable attribute to 'false', you can simply create a string array of the property names and provide them in an overridden property PropertiesToRemove.

C#
protected override System.Collections.Generic.IEnumerable<string> PropertiesToRemove
{
   get { return new string[] { "Text" }; }
}

Easy access properties and event handlers

C#
// Gets the behavior service
public BehaviorService BehaviorService { get; }

// Gets the service that indicates when a new component is selected
public IComponentChangeService ComponentChangeService { get; }

// Gets a service that allows determination of selected items within the designer
public ISelectionService SelectionService { get; }

// Override to react to changes in selected items
protected virtual void OnSelectionChanged(object sender, EventArgs e)

// Override to react to component selection or deselection
protected virtual void OnComponentChanged(object sender, ComponentChangedEventArgs e)

ComponentDesigner Extension Methods

C#
// Shows the UITypeEditor for editing the named property on the specified component
object EditValue(object objectToChange, string propName);

// Displays the specified form from the the design surface
DialogResult ShowDialog(Form dialog)

Points of Interest

It is impossible, as far as I can tell, to work around placing action methods or properties directly within the DesignerActionList implementation.

History

July 14, 2015 - First submission
July 16, 2015 - Added key words for better search engine access

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Chief Technology Officer
United States United States
I have been a Windows software developer since 1991. Most of what I create fills the need for some aspect of bigger projects that I consult on.

Comments and Discussions

 
-- There are no messages in this forum --