This article describes adding design time support for a
TabControl-like component which renders the same way the
Project Properties in Visual Studio 2012.
This is the first time I've tried to make more advanced use of component designers so there are going to be areas that I'm not aware of or have not implemented correctly. The component seems to be working fine, but it's entirely possible that bugs exist, which could cause problems. Caveat emptor!
For this article, I'm not going to delve into how the control itself was put together as I want to focus on the design time support, so I'm just going to provide a quick overview.
TabList- the main control
TabListPage- these are hosted by the
TabListto provided multi-paged support
TabListControlCollection- a custom
TabListPages, and prevents adding other controls directly onto the
TabListPageCollection- a strongly typed wrapper for
The basics of these four classes are all based on the
TabControl. If you know how to use that, then you know how to
TabList control, some property names have changed but
otherwise it's pretty similar.
For rendering support, we use these classes:
ITabListPageRenderer- interface to be implemented by rendering classes
TabListPageRenderer- base class to inherit for render support, and also provides a default renderer property
TabListPageState- flags which describe the state of a
DefaultTabListPageRenderer- simple renderer which draws a header in a Visual Studio 2012-esque style.
And finally, we have the two designers which this article will concentrate on:
TabListDesigner- designer class for the
TabListPageDesigner- designer class for the
TabList control is a container control, we can't use
ControlDesigner. Instead, we'll use
ParentControlDesigner which has a bunch of extra functionality
Normally, I initialize a component via the constructor of the
control. This is fine when you're initializing properties to
default values, but what about adding child items? Consider for
TabControl. When add one of these to a form, it
generates two hosted pages. If you remove these, they don't come
back. If you've ever looked at the designer generated code for a
control, you'll see it will add items to a collection, but
doesn't clear the collection first so creating items via the
initialization method of a component would be problematic.
Fortunately for us, the designer has two methods you can
InitializeNewComponent is called when you create a
new instance of the designed type.
can be used to modify an existing component. There's also a
InitializeNonDefault although I'm not sure
when this is called.
For our purposes, overriding the
Now, whenever you add a
TabList control onto a designer
surface such as a
Form, it'll get two shiny new
For our designer, we need to know when certain actions occur so
we can act accordingly - for example, to disable the Remove verb
if there's nothing to remove. We'll set these up by overriding
The first event we attached as
ISelectionService.SelectionChanged. This event is raised when
the selected components change. We'll use this event to
automatically activate a given
TabListPage if a control hosted
upon it is selected.
The second event
raised when the
RaiseComponentChanged method is called. We'll
describe how this method works a bit further on, but for now, we
use the event to determine if there are any tab pages in the
control - if there are, the remove command is enabled, otherwise
it's disabled. (We'll also describe the verbs further down too!)
The final event,
TabList.SelectedIndexChanged is on the
TabList control itself. We use this event to select the
TabList component for designing due to how component selection
seems to work when you mix runtime and design time
I mentioned verbs above, but just what are they? Well, they are
commands you attach to the context and tasks menu of controls.
To do this, override the
Verbs property of your designer and
create a verbs collection.
Each verb binds to an event handler. For our purposes the events are simple and just pass through into other methods.
I suppose you could just use an anonymous delegate instead.
If you are making multiple changes a control, and one of these goes wrong, the IDE won't automatically undo the changes for you and you will need to handle this yourself. Fortunately, the IDE does provide the facility via designer transactions. In additional to providing a single undo for a number of operations, using transactions can also be good for performance as UI updates are delayed until the transaction is complete.
The code below is called by the Add verb and adds a new
TabListPage to the control.
These are the basic steps for making changes:
- Create a transaction via
- Notify the designer of impending changes via the
- Make the change
- Notify the designer that the change has been made via the
RaiseComponentChangedmethod. This will raise the
IComponentChangeService.ComponentChangedevent mentioned above.
In this case, despite wrapping the transaction in a
statement, I've got got an explicit
catch block to
cancel the transaction in the event of an error. I'm not sure if
this is strictly necessary however.
The handler for the remove verb does pretty much the same thing,
except we use
IDesignerHost.DestroyComponent to remove the
TabList control is selected and you try to drag a
control on it, you'll get an error stating that only
TabListPage controls can be hosted. By overriding the
CreateToolCore method, we can intercept the control creation,
and forward it onto the current
TabListPage via the
CreateToolCore prevents the control from
being created on the
TabList. The reminder of the logic
forwards the call onto the selected
TabListPage, if one is
As you'll have noticed, most controls can't be used at design
time - when you click a control it just selects it. This default
behaviour is a serious problem for our component as if you can't
active other pages, how can you add controls to them?
Fortunately, this is extremely easy to implement as the designer
GetHitTest method which you can override. If you
true from this method, then mouse clicks will be
processed by the underlying control instead of the designer.
In the above code, we translate the provided mouse co-ordinates
into the client co-ordinates, then test to see if they are on
the header of a
TabListPage. If they are, we return
the call will then be forward onto the
TabList control which
will then selected that page.
There is one side effect of this behaviour. As we have
essentially intercepted the mouse call, that means the
control isn't selected. This behaviour is inconsistent with
standard behaviour and this is why when the designer was
initialized we hooked into the
SelectedIndexChanged event of
TabList control. With this hooked, as soon as the
SelectedIndex property is changed we can manually select the
TabList control. Of course, if you'd rather, you could change
that code to select the active
TabListPage instead, but again
that's inconsistent with standard behaviour.
Unfortunately there's also another side effect I discovered -
the context menu no longer works if you right click on an area
where you allow mouse clicks to pass through. Again, this is
fairly straightforward to work around by overriding
and intercepting the
Note: Normally I wouldn't use "magic numbers" as I have here. But at the same time, I don't want to define WM_CONTEXTMENU in this class - for my internal projects, I link to an assembly I've created which contains all the Win32 API functionality that I use. Linking that to this not possible for this example and I don't want to create a
Nativeclass for a just a single member. So this time I'll cheat and leave an inline magic number.
The final side effect I've found is double clicking to open the default event handler doesn't work either.
The final section of the
TabListDesigner class I want to
discuss is design time painting. Normally, in the
overriding of my control, I would have a block similar to the
While there's nothing wrong with this approach, if you are using
a designer than you have another option, which saves you having
to do design time checks each time your contain is painted at
runtime. The designer has an
OnPaintAdornments method, just
override this to perform your design time drawing.
TabList doesn't have a border property, I draw a dotted
line around the control using
TabListPage control is basically a
control with a bunch of properties and events hidden, it still
needs a designer to override some functionality. For the
TabListPageDesigner class, we'll inherit from
TabList control takes care of sizing its child
TabListPage controls, we don't really want the user to be able
to resize or move them at design time. By overriding the
SelectionRules property, you can define exactly which handles
are displayed. As I don't want the control to be moved or sized,
I get rid of everything via the
CanBeParentedTo method is used to determine if a component
can be hosted by another control. I'm overriding this to make
sure that they can only be parented on another
control. Although, as I've disabled the dragging of
TabListPage controls with selection rules above, you can't
drag them to reparent anyway.
- As described above, if you double click one of the
TabListPageheaders nothing happens. Normally, you'd expect a code window to be opened at the default event handler for the control. While it should be possible to trap the
WM_LBUTTONDBLCLKmessage, I don't know how to open a code window, or create a default event handler is one is missing.
- Another issue I spotted is that I can't Cut (or Copy) a
TabListcontrol to another. Not sure why yet, but I'll update the source on GitHub when I fix it.
Get the source code from the link below. I've also uploaded it to GitHub, feel free to fork and make pull requests to make this component even better!
- 2012-08-19 - First published
- 2020-11-21 - Updated formatting
Like what you're reading? Perhaps you like to buy us a coffee?