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:
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.
[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.
[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.
[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.
[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:
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.
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.
protected override System.Collections.Generic.IEnumerable<string> PropertiesToRemove
{
get { return new string[] { "Text" }; }
}
Easy access properties and event handlers
public BehaviorService BehaviorService { get; }
public IComponentChangeService ComponentChangeService { get; }
public ISelectionService SelectionService { get; }
protected virtual void OnSelectionChanged(object sender, EventArgs e)
protected virtual void OnComponentChanged(object sender, ComponentChangedEventArgs e)
ComponentDesigner Extension Methods
object EditValue(object objectToChange, string propName);
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