Introduction
Ever since I first started programming with OWL back in 1995, I've written
this type of control each time I learn a new language in which to program GUIs.
I wrote one in OWL (Borland's MFC-like library), in straight C, and Java 2's
Swing, among others. I've always called them 'PropertyTree's - mostly because
the first place I saw them implemented was in Netscape's Edit | Properties
dialog box. There are already a number of controls like this (using MFC)
available on CodeProject - Chris Losinger's SAPrefs control, and
Sven Wiegand's CTreePropSheet.
This PropertyTree
, however, is written to integrate with Visual
Studio .NET's spiffy design-time environment. It's written in C#, but can be
used by any .NET language. In order to make use of the design-time functionality
of PropertyTree
, however, you'll need to be using VS.NET.
If you don't want to, you never have to actually write one line of code to
setup your PropertyTree
- you just add PropertyPane
s
and then drag controls onto them like you would drag controls onto a
TabPage
.
Update Notice
This article covers the newer 2.0.1.0 (Alpha) release of
PropertyTree
. The code has gone through a complete rewrite since
version 1.0. Much of the functionality is still the same (with some added
functionality, of course), but the way that it is acheived in code has changed
dramatically.
These changes in PropertyTree
2.0 mean that it is completely
incompatible with PropertyTree
0.9 and 1.0 code. The main reasons
will be outlined or explained in the article.
Terminology
Before getting started we'll introduce the three main classes:
PropertyTree
, PropertyPane
, and PaneNode
.
PropertyTree
is the class that contains the TreeView
,
and PropertyPane
is a container control that ends up being
associated with some node in the PropertyTree's
TreeView
. PaneNode
is a class that is responsible for
representing a particular PropertyPane
as a node in the
PropertyTree
. When a node is selected in the
PropertyTree's
TreeView
, its corresponding
PaneNode
is used to get a PropertyPane
which is then
displayed to the right of the TreeView
.
While this discription may sound a bit convoluted - just think of a
PropertyTree
as a TabControl
that uses a
TreeView
instead of a row of notebook tabs.
Using PropertyTree
PropertyTree
is built as a self-contained 3rd party control, and
a signed assembly (WRM.PropertyTree.dll) is made available in the download links
above so that you can simply plop it onto your system, add it to your VS.NET
ToolBox, and start using it right away. Of course, the source is provided so
that you can tinker with it and build your own versions of it as you like - but
you don't have to if you don't want to mess with all of that.
If you want to use PropertyTree
in the VS.NET WinForms designer
(which you probably do), you'll first need to right click on the VS.NET Toolbox
and select "Customize ToolBox...". In the ensuing dialog, take these steps:
- Select the ".NET Framework Components" tab
- Click the "Browse..." button
- Navigate to and select the WRM.PropertyTree.dll assembly
- Make sure that the "PropertyTree" and "PropertyPane" controls are checked.
- Press OK until you're back in VS.NET
If you aren't looking to use the VS.NET WinForms designer at all, you don't
need to take these steps. You can simply create the PropertyTree
like any other control. You will, however, be constrained to the "Custom
PropertyPane" and "SharedPropertyPane" design scenarios.
Design scenarios - creating PropertyPanes
There are three main design scenarios supported by PropertyTree
.
All are supported by VS.NET's WinForms designer, and two of the three are
available without it.
Scenario |
Availability |
Description |
Anonymous PropertyPane s |
WinForms designer |
Instances of PropertyPane are created and added to
the PropertyTree at design-time. Controls are dragged from the
VS.NET ToolBox and are dropped onto these PropertyPane instances in
the same way that controls are dropped onto TabPage s in a
TabControl . |
Custom PropertyPane s |
WinForms designer, regular code |
Programmer derives classes from UserPropertyPane and
designs them in the same way that he would design a
UserControl -derived class. These "Custom Panes" can then be added
to a PropertyTree with the WinForms designer (by dragging them from
the "PropertyPanes" tab group on the ToolBox) or by regular code. |
Shared PropertyPane s |
WinForms designer (no PropertyTree interaction),
regular code |
Programmer derives classes from SharedPropertyPane
and designs them in the same way that he would design a
UserControl -derived class. These "Shared Panes" can then be added
to a PropertyTree by regular code. (See the section on the Shared Panes design
scenario below for more details as to why they cannot be added to a
PropertyTree at design-time.) |
Anonymous PropertyPanes
This design scenario is only available when using PropertyTree
in the VS.NET WinForms designer. This is because this scenario is entirely built
around the WinForms designer's ability to drag-n-drop controls around on the
design surface to design your UI. VS.NET uses the custom designer components for
PropertyTree
and PropertyPane
to enable the programmer
to visually edit and rearrange the PropertyPane
s in the
PropertyTree
, and the controls on each of the
PropertyPane
s.
In order to add an anonymous PropertyPane
to a
PropertyTree
, you simply right-click in the TreeView
area of the PropertyTree
to bring up the context menu. Selecting
"Add PropertyPane..." adds a new anonymous PropertyPane
as a
sibling of the PropertyPane
currently selected in the
PropertyTree
. Selecting "Add PropertyPane as child" adds a new
anonymous PropertyPane
as a child of the PropertyPane
currently selected in the PropertyTree
. Selecting "Remove
PropertyPane" will remove the currently selected PropertyPane
from
the PropertyTree
To add controls to an anonymous PropertyPane
, select its node in
the PropertyTree
. Notice that the PropertyPane
area to
the right of the PropertyTree
has the same grid background as a
Form does. This is because you can add controls to the selected
PropertyPane
via drag-n-drop the same way you can for any other
container control.
You would use anonymous PropertyPane
s when each one is going to
be unique, and where the logic involved on those panes is relatively simple. The
second point deserves emphasis: The events fired by controls hosted on an
anonymous PropertyPane
are all handled in the code for the control
that hosts the PropertyTree
that contains those anonymous
PropertyPane
s. If you've got a fair amount of controls on your
anonymous PropertyPane
s, or if they involve a fair amount of logic,
your form code can quickly become large and muddled. When this appears to be the
case, you should really use the Custom PropertyPane
design scenario
to better encapsulate and insulate the behavior of each of your panes.
Custom PropertyPanes
This design scenario is available regardless of whether or not you use the
VS.NET WinForms designer. Of course, it is streamlined by using the WinForms
designer, but it is not completely necessary. In this design scenario, the
programmer creates custom PropertyPane
s by deriving them from
UserPropertyPane
, and then adds instances of those new custom
PropertyPane
classes to the PropertyTree
(either by
using the WinForms designer, or at runtime).
Because PropertyPane
is just a class derived from
UserControl
, you can derive your own class from it and use it just
about anywhere where a PropertyPane
would be used. In VS.NET, the
design view of this custom PropertyPane
-derived class is the same
as the design view for a UserControl
. So all you have to do is
design your custom PropertyPane
-derived class like you would any
regular UserControl
class. When you are done, build your
project.
Notice that there is now a new tab group on the VS.NET Toolbox called
"PropertyPanes". Every one of the classes in this project that derives from
PropertyPane
(but not from SharedPropertyPane
, see below) is added
to this tab group. You can then add instances of your custom
PropertyPane
-derived classes to a PropertyTree
by
dragging these Toolbox items and dropping them onto the TreeView
of
the PropertyTree
to which you want to add them.
As a side note: The PropertyPaneRootDesigner
updates the
ToolboxItem
in the "PropertyPanes" tab group whenever it is
instantiated. Therefore, no ToolboxItem
will appear in the
"PropertyPanes" tab group for a custom PropertyPane
-derived class
until the design view of that class has been openned once. So, if you don't see
your custom PropertyPane
in that tab group, try re-openning its
design view and checking again.
You would use the custom PropertyPane
design scenario whenever
you think that a certain pane will involve complex, localizable logic, or when
different instances of the pane will need to be used at the same time. However,
when it starts to look like lots and lots of instances of one pane will be used,
you should probably look at the shared PropertyPane
design
scenario.
Shared PropertyPanes
This design scenario is only partly supported by the VS.NET WinForms
designer. Specifically, shared PropertyPane
s can be created and
designed in the same way as custom PropertyPane
s, but they cannot
be added to a PropertyTree
at design-time. This is because shared
PropertyPane
s - which derive from SharedPropertyPane
-
are a bit of a special case. Without going into too much detail here, there is a
potentially one-to-many relationship between a single instance of a
SharedPropertyPane
and nodes in a PropertyTree
. This
many-to-one relationship is resolved by use of the PaneNode
utility
class, which will be discussed below. So, the only way to add a shared
PropertyPane
to a PropertyTree
is to do so at runtime
using regular code.
You would use the shared PropertyPane
design scenario whenever
you think that many, many "instances" of a certain PropertyPane
will be necessary. In this scenario, you build a custom object that houses all
of the information that your shared PropertyPane
would need, and
then the PropertyTree
takes care of shuffling instances of that
data object into one single instance of your shared PropertyPane
.
This way, only one real instance of the shared PropertyPane
is ever
created, but it can act as though there is a separate instance for each data
object.
A good example of this would be an Explorer-like app. You would create a
FileInfoPane, derived from SharedPropertyPane
. The custom data
object, in this case, would just be a FileInfo object, perhaps. As the user
clicked through the nodes in the PropertyTree
,
PropertyTree
would just keep changing the FileInfo object that the
FileInfoPane instance was working with.
Common design-time behavior
While the three different scenarios above offer different functionality,
there is some functionality that is always available in the WinForms
designer:
- To select a
PropertyPane
, click on its node in the
PropertyTree
. The PropertyPane
will appear in the area
to the right of the TreeView
.
- To change properties of a
PropertyPane
, select its node in the
PropertyTree
, and then click on the PropertyPane
area.
This will select the PropertyPane
in the "Properties" window.
- To rearrange the heirarchy of
PropertyPane
s in the
PropertyTree
, simply click the node of the
PropertyPane
you want to move and drag it to where you want it
moved. Dropping the PropertyPane
will make it a sibling of the node
that it is dropped on. Holding down the Control key when you drop, however,
makes the dragged PropertyPane
become a child of the
PropertyPane
it is dropped on.
- To remove a
PropertyPane
form the PropertyTree
,
right click on its node in the PropertyTree
and select "Remove
PropertyPane" from the context menu.
Common run-time behavior
All PropertyTree
functionality is available at run-time. The
Anonymous PropertyPane
s scenario doesn't make much sense without
the designer, but it can nevertheless be employed directly in code if you really
want to. Most likely, however, you will use the run-time
PropertyTree
functionality to add, rearrange, or remove Custom or
Shared PropertyPane
s from a PropertyTree
.
Working with PropertyPanes
Regardless of how you design your PropertyPane
s (anonymous,
custom, shared), you can always fiddle with their properties and with their
placement at runtime. This biggest difference between this version of
PropertyTree
(2.0) and the previous versions is that this version
does not use file-system-like Path strings to indicate a
PropertyPane
's position in a PropertyTree
.
PropertyTree
2.0 uses the more natural TreeNode-like approach in
conjunction with a PaneNode
class that acts much like a
TreeNode.
PaneNode
The PaneNode
class's main job is to represent the placement of a
particular PropertyPane
in the PropertyTree
. An
instance of a PropertyPane
class itself cannot do this job, because
a SharedPropertyPane
instance can be referenced by multiple "nodes"
in the PropertyTree
. The PaneNode
class is equiped to
keep information about either a regular PropertyPane
or a
SharedPropertyPane
.
Standing in for a PropertyPane
PaneNode
contains many properties that represent intrinsic
properties on a PropertyPane
. For instance, things like .Title,
.ImageIndex, and .SelectedImageIndex. When a PaneNode
represents an
instance of a regular PropertyPane
-derived class (i.e. one not
derived from SharedPropertyPane
), properties like these map
directly to the corresponding property of the PropertyPane
-derived
class referenced by the .PropertyPane
property. When a
PaneNode
represents an instance of a
SharedPropertyPane
class, these properties are stored locally by
the PaneNode
instance, because many PaneNode
objects
may reference that one instance of the SharedPropertyPane
-derived
class.
There are other non-aggregated properties contained by PaneNode
.
When a PaneNode
represents an instance of a
SharedPropertyPane
, its .IsShared property is true, and its .Data
property contains a reference to the custom data object that will be passed to
the SharedPropertyPane
instance referenced by the
.PropertyPane
property when this PaneNode
is selected.
The .IsShared property is false whenever the PropertyPane
class
referenced by .PropertyPane
is not derived from
SharedPropertyPane
. In this case, the .Data value is null, and
should be completely ignored.
Adding and removing PropertyPanes
The PaneNode
class's other big job is to represent the
heirarchical relationship of PropertyPane
s in the
PropertyTree
. A PaneNode
's child PaneNodes exist in
its .PaneNodes collection. Adding PaneNodes to, or removing them from this
collection will affect their placement in the PropertyTree
. This is
in stark contrast to the 'path' strings that previous versions of
PropertyTree
used.
For example: The following code adds some PropertyPane
s as
children of a PaneNode
, then removes them and makes them children
of another PaneNode
:
PaneNode rootNode1 = propertyTree.PaneNodes.Add(new MyCustomPropertyPane());
PaneNode rootNode2 = propertyTree.PaneNodes.Add(new MyCustomPropertyPane());
PaneNode child1 = rootNode1.PaneNodes.Add(new MyOtherCustomPane());
PaneNode child2 = rootNode1.PaneNodes.Add(new MyOtherCustomPane());
rootNode1.PaneNodes.Remove(child1);
rootNode1.PaneNodes.Remove(child2);
rootNode2.PaneNodes.Add(child1);
rootNode2.PaneNodes.Add(child2);
It's important to note that you can work with the PaneNodes the way the code
above does regardless of whether or not they are currently in a
PropertyTree
.
PaneNodeCollection
The PaneNode.PaneNodes
property always references a
PaneNodeCollection object. This object is a specialized container built
specifically to deal with adding and removing PropertyPane
related
things. The .Add method is overloaded to handle adding PropertyPane
instances, PaneNodes, and SharedPropertyPane
instances.
Add(PropertyPane pane)
Add(PropertyPane pane, int index, int imageIndex, int selectedImageIndex)
You would use these overloads of Add
to add a newly created
PropertyPane instance. This would return a PaneNode
object that
represents that PropertyPane instance in the PropertyTree
. The
index
parameter identifies the zero-based index at which to insert
pane
. Setting this to -1 inserts pane
at the end of
the list.
Add(PaneNode paneNode)
You would use this overload to add an existing PaneNode
. The
existing paneNode
cannot already exist as a node in this or any
other PropertyTree
Add(Type sharedPaneType, string title, object data)
Add(Type sharedPaneType,
string title,
int index,
int imageIndex,
int selectedImageIndex,
object data)
You would use one of these two overloads to add a PaneNode
that
represents the SharedPropertyPane
of type
sharedPaneType
. Behind the scenes, PropertyTree
creates an instance of sharedPaneType
and keeps it around for as
long as a PaneNode
is referencing it. The index
parameter identifies the zero-based index at which to insert the
PaneNode
representing sharedPaneType
. Setting this to
-1 inserts the PaneNode
at the end of the list.
The other collection-style methods of PaneNodeCollection are self
explanatory. Their function and purpose is the same as any IList's - the only
difference is that they are strongly typed to deal only with
PaneNode
instances.
PropertyPanes and selection events
PropertyTree
has a rather involved selection/deselection
process. It allows both the current PropertyPane
and the newly
selected PropertyPane
to Ok the selection change before the change
actually takes place. If either of the PropertyPane
s veto the
selection change, no selection change will be made. If both agree, each is
notified again after the selection change has occurred. The order of the
selection events is:
- PaneDeactivating (vetoable - involves currently selected
PropertyPane
)
- PaneActivating (vetoable - involves newly selected
PropertyPane
- PaneDeactivated (involves currently selected
PropertyPane
)
- PaneActivated (involves newly selected
PropertyPane
)
The PropertyTree
fires its four events during the selection
process. This allows the form that hosts the PropertyTree
to have a
say in the selection process as well. When you are working with anonymous
PropertyPane
s, these PropertyTree
events are where you
must handle the selection change process.
When you are working with custom or shared PropertyPane
s, each
of these types of PropetyPane instances involved in the selection change process
will have their On[SelChangeEventName]()
methods called by
PropertyTree
. This allows the selection change process to be
handled internal to that PropertyPane
-derived class. Even though
the PropertyTree
will still fire its four events, overriding the
On[SelChangeEventName]()
methods in your custom
PropertyPane
-derived class is the preferred method for handling the
selection change process.
During the PaneDeactivating or PaneActivating events, setting the
PaneSelectionEventArgs.Cancel property to true will veto the selection change.
The .Cancel property is a logical OR of all the values it has been set to. This
is so that any one of the possibly multiple event listeners can veto the
selection change.
PropertyTree
's selection change process is an "opt out" process.
By default, all selection changes are Ok. It is only by explicitly handling the
selection change events that you can veto it. If you don't ever need to veto
selection changes, you can safely ignore the entire process.
PropertyTree functionality, bells and whistles
With the main task of creating and arranging PropertyPane
s
explained, there is still some extra functionality of PropertyTree
to discuss.
The root PaneNodes
PropertyTree
defines its own .PaneNodes property. The
PaneNodeCollection this property references contains all of the root-level
PaneNodes for this PropertyTree
.
Programmatic selection change
PropertyTree
defines the read/write property
SelectedPaneNode
to reference the PaneNode
that is
currently selected in the PropertyTree
. A value of null indicates
no selection at all.
At any point, you can manipulate which PaneNode
is currently
selected in the PropertyTree
by setting this value to either a
valid PaneNode
object that exists in the PropertyTree
,
or null. Setting this value initiates the pane selection process (discussed above),
which can possibly be vetoed. If the pane selection process is vetoed, then the
SelectedPaneNode
property's value will not change.
PaneNode images
PaneNodes have an ImageIndex and SelectedImageIndex
property
associated with them that select images from the PropertyTree
's
ImageList. These properties shadow the underlying TreeNode's properties of the
same names.
AutoNavigate
One feature that I really like is PropertyTree
's emulation of
the option browser as it looks in VS.NET (choose Tools | Options...). In this
mode of operation, all but the selected PaneNode
and its direct
ancestors are collapsed automatically. The SysTreeView32 has a window style that
does this automatically (TVS_SINGLEEXPAND), but I chose to emulate the behavior
manually to keep from having to worry about which version of the common controls
was on the system.
Set AutoNavigate
to true to make the PropertyTree
exhibit this behavior. Note that this will change the ImageList the
PropertyTree
works with, update all of the PaneNode
's
ImageIndex and SelectedImageIndex values, and will turn all line-drawing and
plus/minus box drawing off in the TreeView
.
Implementation details - how the code works
The code itself is sitting at around 6000 lines of source & comments, so
I'm only going to go over the most interesting parts of its design here. If you
are interested in the guts of PropertyTree
, dig through the source
code - it is heavily commented. I wrote PropertyTree
as a learning
excercise for myself, and I'd like other people to be able to use it in the same
way.
TreeNode -> PaneNode -> PropertyPane
The basic concept of these types of controls is that a particular set of
child controls is displayed to the user when they click on a node in a
TreeView
. From this it is clear that we have two endpoints here -
node in the TreeView
, and the container of the child controls. For
PropertyTree
, these two endpoints are the TreeNode object and the
PropertyPane
control.
In simple scenarios, mapping from a TreeNode directly to a
PropertyPane
instance is somewhat feasable. The biggest problem
with this idea, however, is the fact that it requires a separate instance of
each PropertyPane
. This is not a problem for "option setting" style
dialogs, where each PropertyPane
ostensibly contains a different
smattering of controls. However, for building explorer-style apps it is not
acceptable. There may be 100 nodes in the tree, but only two different types of
PropertyPane
-derived classes. In this case, it would be much more
efficient to have only one instance of each type of
PropertyPane
-derived class and just hand it new data as new nodes
were selected.
This concept of the Shared PropertyPane
(as discussed above)
violates the previous constraint that every TreeNode maps directly to an
instance of a PropertyPane
. Because of this, an intermediary object
needs to be introduced to provide a level of indirection between TreeNode and
PropertyPane
. This class, called PaneNode
, will have a
one-to-one relationship with nodes in the TreeView
(as
PropertyPane
did in the simpler scenario), and will allow for (but
not insist upon) a many-to-one relationship with PropertyPane
instances. In addition, if the PaneNode
represents a shared
PropertyPane
, the PaneNode
class contains that node's
"data object" as well.
Shadowing .Controls - a poor man's covariance
For container-style controls, the Controls
property contains a
collection (of type Control.ControlCollection) of all of that Control's child
Controls. This presents a bit of a problem for any user-defined Control subclass
that intends to host only a particular type of Control subclass. The problem is
that the Controls
property is of type Control.ControlCollection -
which is designed to work with any type of Control-derived class at all.
However, the user-defiend Control subclass only wants to host a particular
subclass of Control.
There are two main stumbling blocks in this situation:
- C# does not support covariant return types
- It wouldn't matter if it did because the
Control.Controls
property isn't virtual.
If both of these problems didn't exist, the code could simply be written like
this:
public class PaneNodeCollection : Control.ControlCollection
{
...
}
public class PropertyTree : UserControl
{
...
public override PaneNodeCollection Controls
{
get
{
return mPaneNodes;
}
}
...
}
Unfortunately, an uglier route has to be taken in order to solve this
problem. TabControl
deals with the problem by deriving its own
container class from Control.ControlCollection
, and exposing it via
its TabPage
s property. This works well for TabControl
because its collecion of child controls is homogenous - they are all
TabPage
s. All TabControl
does is make sure that the
WinForms designer doesn't attempt to serialize the contents of its
TabPages
collection, because it has the same contents as the
Controls
collection.
PropertyTree
, however, has a heterogenous Controls
collection: not only does it contain PropertyPane
s, it also
contains a TreeView
, some Label
s, and some other
controls. During code serialization, the WinForms designer inspects the contents
of the Controls
collection and attempts to match the object
instances it finds in there with object instances that it knows have been
created as a part of the design session. However, when it gets ahold of the
TreeView
in the PropertyTree.Controls
collection, it
doesn't recognize that object instance (because PropertyTree
created it without ever telling the WinForms designer about it). Once this
happens, the WinForms designer just gives up and doesn't serialize the
Controls
property at all. The end result, of course, is that the
Controls
collection would never get serialized.
PropertyTree
0.9 and 1.0 got around this by redefining the
PropertyTree.Controls
collection to return the
Controls
collection of an internal Panel that actually served as
the PropertyPane
s' parent. This worked perfectly well, but it was
not an optimal solution because of the fact that the Controls
control collection was still only typed to work with
Control
-derived objects. It would be much better if
PropertyTree
had its own collection designed specifically to deal
with PropertyPane
s and related objects.
This custom collection, described in the PaneNodeCollection
section above, is designed to do just that. All of its methods are strongly
typed to work with PaneNode
objects, except for the Add method
which has a number of overloads. This custom PaneNodeCollection - exposed by the
PropertyTree.PaneNodes
property - completely takes the place of the
PropertyTree.Controls
collection.
But the WinForms designer will still try to serialize the contents of the
PropertyTree.Controls
collection!. In order to stop this, the
PropertyTreeDesigner
class overrides the
OnPostFilterProperties()
function so that the design-time
environment doesn't even know that the PropertyTree
has a
"Controls" property:
protected override void PostFilterProperties(
System.Collections.IDictionary properties)
{
string[] propertiesToExclude = {"Controls"};
foreach(string prop in propertiesToExclude)
if(properties.Contains(prop))
properties.Remove(prop);
base.PostFilterProperties(properties);
}
Design-time integration
Before getting started, I'd like to reference some pretty good VS.NET
design-time articles and tutorials that I'm aware of.
In VS.NET, a 'Designer' object is associated with each Control that is placed
on a form in the Form designer. For most controls, this Designer object offers
functionality to the control that should only be available during design-time.
For instance, the PropertyTreeDesigner
object adds three
DesignerVerbs to the context menu when the PropertyTree
is
selected, and handles mouse clicks to select and rearrange nodes.
A 'Designer' object type is associated with a Control class by using the
Designer attribute:
[Designer(typeof(WRM.Windows.Forms.Design.PropertyTreeDesigner))]
...
public class PropertyTree : UserControl
{
...
Because of this attribute, the WinForms designer will use a new instance of
PropertyTreeDesigner
for each PropertyTree
that you
drop on the design surface.
The same thing is done for PropertyPane
[Designer(typeof(WRM.Windows.Forms.Design.PropertyPaneDesigner))]
[Designer(typeof(WRM.Windows.Forms.Design.PropertyPaneRootDesigner),
typeof(IRootDesigner))]
Notice that the PropertyPane
class has two Designer attributes
associated with it. The one with one parameter simply gives the
Type
of the Designer object that is to be used for the
PropertyPane
when it is dropped onto a design surface. The other,
which contains a second parameter, gives the Type
of
Designer
object that is to be used when this
PropertyPane
is the design surface. This type of Designer
must implement the IRootDesigner interface. The WinForms designer itself, along
with the DocumentDesigner that UserControl
uses, are examples of
these Designer objects that serve as the designer surface.
So, the PropertyPaneDesigner
object is used to drive the
design-time experience for a PropertyPane
when it exists on some
other design surface (e.g. when it exists inside a PropertyTree
that exists on some Form or other Control at design-time). The
PropertyPaneRootDesigner
, which is derived from DocumentDesigner,
serves as the design surface itself for design your custom or shared
PropertyPane
s.
PropertyTreeDesigner - interesting bits
Adding Verbs to the context menu
Every Designer object has a Verbs property which is a collection of menu
commands that VS.NET will add to the context menu when that control is selected.
PropertyTreeDesigner
adds three DesignerVerbs
:
- Add PropertyPane
- Add PropertyPane as child
- Remove PropertyPane
The Verbs
property is populated and implemented in
PropertyTreeDesigner
with this code:
public PropertyTreeDesigner()
{
mVerbs = new DesignerVerbCollection();
mVerbs.Add(new DesignerVerb("Add PropertyPane",
new EventHandler(OnAddPane)));
mVerbs.Add(new DesignerVerb("Add PropertyPane as child",
new EventHandler(OnAddPaneAsChild)));
mVerbs.Add(new DesignerVerb("Remove PropertyPane",
new EventHandler(OnRemovePane)));
mVerbs[2].Enabled = false;
...
}
...
public override DesignerVerbCollection Verbs
{
get
{
return mVerbs;
}
}
Mouse handling
A big part of the design-time functionality of PropertyTree
comes from handling user mouse clicks. The TreeView
, in design
mode, does not allow the user to select nodes by clicking on them. In order to
allow people to select nodes in the TreeView
(and thus display
their corresponding PropertyPane
),
PropertyPaneDesigner
has to intercept the mouse clicks and manually
select the node. In addition to this, it also needs to write code to implement
drag-and-drop so that nodes can be rearranged in the tree, and so that
PropertyPane
ToolBox items can be dragged from the ToolBox and
dropped onto the PropertyTree
.
PropertyTreeDesigner
does this by overrideing the WndProc method
of ControlDesigner
. By intercepting mouse events, it can force the
TreeView
underneath to respond as we would like it to respond to
user input during design-time.
protected override void WndProc(ref Message m)
{
if(mPropertyTree.TreeView.Created &&
(m.HWnd == mPropertyTree.TreeView.Handle ||
m.HWnd == mPropertyTree.Handle) )
{
switch(m.Msg)
{
case WM_LBUTTONDOWN:
...
break;
case WM_MOUSEMOVE:
...
break;
case WM_LBUTTONUP:
...
break;
case WM_RBUTTONDOWN:
...
break;
case WM_RBUTTONUP:
...
break;
default:
base.WndProc(ref m);
break;
}
}
else
{
base.WndProc(ref m);
}
}
Creating new control instances via IDesignerHost
When the user selects one of the two 'Add PropertyPane' verbs, or drops a
custom PropertyPane
onto the PropertyTree
from the
ToolBox, the PropertyTreeDesigner
needs to add a new
PropertyPane
instance to the PropertyTree
. But, it
must also make sure that the WinForms designer knows about the new
PropertyPane
instance. This is so that it can associate a unique
name (i.e. the name of the variable that references it) with that control
instance and then generate code to create that control and set its
properties.
In order to keep the WinForms designer in-the-loop about this,
PropertyTreeDesigner
uses the
IDesignerHost.CreateComponent
method to create new
PropertyPane
instances. (note that IDesignerHost is a service
interface made available to Designer objects by the WinForms designer). When a
PropertyPane
is created with CreateComponent
, the
WinForms designer knows about its existence and will take care of generating the
code to create it and set its properties in InitializeComponent.
public void OnAddPane(object sender, EventArgs e)
{
string name = GenerateNewPaneName();
...
PropertyPane pp =
mDesignerHost.CreateComponent(typeof(PropertyPane),name)
as PropertyPane;
pp.Text= name;
PaneNode paneNode = null;
if(parentNode == null)
paneNode = mPropertyTree.PaneNodes.Add(pp);
else
paneNode = parentNode.PaneNodes.Add(pp);
mPropertyTree.SelectedPaneNode = paneNode;
}
Responding to ToolBox drag-n-drop
The custom PropertyPane
scenario centers on building your
PropertyTree
at design-time by dragging and dropping custom
PropertyPane
-derived classes from the ToolBox onto the
PropertyTree
. The PropertyPaneRootDesigner
(see
below) takes care of making sure that the ToolBox item is put into the
ToolBox. The PropertyTreeDesigner
simply needs to respond to
regular old OLE drag-n-drop events, keeping an eye out for ToolBox items. It
does this via the following code:
protected override void OnDragEnter(System.Windows.Forms.DragEventArgs de)
{
base.OnDragEnter(de);
IDataObject data = de.Data;
if(!mToolboxService.IsToolboxItem(data))
{
de.Effect = DragDropEffects.None;
return;
}
ToolboxItem ti = (ToolboxItem)mToolboxService.DeserializeToolboxItem(data);
Type t = Type.GetType(ti.TypeName);
if(t == null)
{
de.Effect = DragDropEffects.None;
return;
}
if(typeof(PropertyPane).IsAssignableFrom(t) &&
!typeof(SharedPropertyPane).IsAssignableFrom(t))
de.Effect = DragDropEffects.Copy;
else
de.Effect = DragDropEffects.None;
}
PropertyPaneDesigner
The PropertyPaneDesigner
isn't all that interesting. It is
derived from ParentControlDesigner
, so that it can host other
controls that are dropped onto it during design-time. Because it is derived from
ParentControlDesigner
, its background during design-time is a grid
- just like a Form's background. This can make it hard for the user to
distinguish the PropertyPane
from the Form itself. Because of this,
the PropertyPaneDesigner
overrides the
OnPaintAdornments
method of ControlDesigner
to paint a
dashed border around the area of the PropertyPane
.
protected override void OnPaintAdornments(System.Windows.Forms.PaintEventArgs pe)
{
base.OnPaintAdornments(pe);
...
pe.Graphics.DrawRectangle(mBorderPen,
1,1,mPropertyPane.Width-2,mPropertyPane.Height-2);
}
PropertyPaneRootDesigner
The PropertyPaneRootDesigner
is a little more interesting. It's
primary purpose is to make sure that there is a ToolBox item that represents
that custom PropertyPane
. This ToolBox item can be dragged from the
VS.NET ToolBox and dropped onto a PropertyTree
, thus creating an
instance of that PropertyPane
in that PropertyTree
.
This is the custom PropertyPane
design scenario.
The PropertyPaneRootDesigner
has a pretty straightforward
algorithm for adding the ToolBox item:
- Determine
Type
of designed PropertyPane
object
- Do not add ToolBox item if the
Type
is derived from
SharedPropertyPane
- Otherwise, determine the display name and the bitmap to use for the ToolBox
item
- Display name is class name without namespace
- Determine Bitmap to use
- Search for [Display name].bmp in resources
- Use default gray arrow bitmap
- Remove any existing ToolBox item that matches this one
- Add this ToolBox item.
The code to do this is as follows (note: the doc comments have been removed
to save space):
private void OnLoadComplete(object sender, EventArgs e)
{
IDesignerHost host = (IDesignerHost)sender;
...
IToolboxService tbx = (IToolboxService)GetService(typeof(IToolboxService));
Type paneType = host.RootComponent.GetType();
if (tbx != null &&
!paneType.Equals(typeof(SharedPropertyPane)) &&
!paneType.IsSubclassOf(typeof(SharedPropertyPane)))
{
string fullClassName = host.RootComponentClassName;
ToolboxItem item = new ToolboxItem();
item.TypeName = fullClassName;
int idx = fullClassName.LastIndexOf('.');
if (idx != -1)
{
item.DisplayName = fullClassName.Substring(idx + 1);
}
else
{
item.DisplayName = fullClassName;
}
item.Bitmap = GetToolboxBitmap(Type.GetType(fullClassName),
item.DisplayName);
item.Lock();
if(tbx.GetToolboxItems().Contains(item))
{
tbx.RemoveToolboxItem(item,"PropertyPanes");
}
tbx.AddLinkedToolboxItem(item, "PropertyPanes", host);
}
}
...
private Bitmap GetToolboxBitmap(Type t, string className)
{
Bitmap b;
try
{
b = new Bitmap(t, className + ".bmp");
}
catch(Exception )
{
b = new Bitmap(typeof(PropertyPane),"PropertyPane.bmp");
}
b.MakeTransparent(Color.FromArgb(0,255,0));
return b;
}
Basically, this code checks to see if the dragged object is a ToolBox item by
using the IToolboxService, which is another one of many Designer support
interfaces provided by the WinForms designer. If it is, it makes sure that the
dragged component it represents is derived from PropertyPane
, but
not from SharedPropertyPane
. If both of these tests pass, then the
drag-n-drop operation is allowed to continue.
Once the drop occurs, quite a bit of rather straightforward code is executed
to figure out what node it was dropped on, what Type
of
PropertyPane
was dropped, etc... And in the end, the new instance
of a PropertyPane
-derived class is created and added to the
PropertyTree
.
PaneNodeCollectionSerializer
The WinForms code serializer will only serialize code for objects that are
visible to it during design-time. This presents PropertyTree
and
PropertyPane
with a bit of a problem - they need to serialize all
PropertyPane
s in code, but their PaneNodes collection contains only
references to PaneNode
objects, which should not be serialized in
code.
One solution to this problem is to make the PaneNode
class
serializable. I did not choose this route, however, because I really didn't
think that PaneNode
was a class that really warranted being
persisted in code. I think of it as more of a runtime by-product.
The other solution uses one of the funkier offerings of the VS.NET
design-time environment: custom code serializers. A code serializer object,
which derives from CodeDomSerializer, knows how to translate between some object
instance and a CodeDom representation of that object instance. As rare a case as
it is, this class is intended to give the Control
author absolute
control over how his Control
is persisted into, and read out of,
source code by the WinForms designer.
A custom CodeDomSerializer is associated with a Control
by using
the CodeDomSerializer attribute:
[DesignerSerializer(typeof(WRM.Windows.Forms.Design.PaneNodeCollectionSerializer),
typeof(CodeDomSerializer))]
When it comes time to regenerate the code in InitializeComponent, the
WinForms designer will create an instance of PaneNodeCollectionSerializer and
allow it to generate the CodeDom representation of a PropertyTree
or PropertyPane
.
The PaneNodeCollectionSerializer itself is actually pretty simple. Its
behavior is no different than the default CodeDomSerializer's except for the
code it generates for the PaneNodes property (both PropertyTree
and
PropertyPane
have a PaneNodes property, which is why this custom
serializer is associated with both of those classes). Instead of attempting (and
failing) to serialize the PaneNode
instances in PaneNodes, it
instead generates code to add the referenced PropertyPane
s to that
PaneNodes collection. For all properties besides the PaneNodes property, the
serialization is left up to the default CodeDomSerializer.
public override object Serialize(
IDesignerSerializationManager manager,
object value)
{
object codeObject = GetBaseSerializer(manager).Serialize(manager, value);
ArrayList topLevelPanes = new ArrayList();
CodeStatementCollection csc = (CodeStatementCollection)codeObject;
PropertyDescriptor paneNodesProp
= TypeDescriptor.GetProperties(value)["PaneNodes"];
PaneNodeCollection nodes = (PaneNodeCollection)paneNodesProp.GetValue(value);
string compName = manager.GetName(value);
foreach(PaneNode child in nodes)
{
CodeThisReferenceExpression thisRef =
new CodeThisReferenceExpression();
CodeFieldReferenceExpression fieldRef =
new CodeFieldReferenceExpression(thisRef,compName);
CodeFieldReferenceExpression childRef =
new CodeFieldReferenceExpression(
thisRef,manager.GetName(child.PropertyPane));
CodePropertyReferenceExpression paneNodesRef =
new CodePropertyReferenceExpression(fieldRef,"PaneNodes");
CodeMethodInvokeExpression invokeExpr = new CodeMethodInvokeExpression(
paneNodesRef,
"Add",
childRef,
new CodePrimitiveExpression(child.Index),
new CodePrimitiveExpression(child.ImageIndex),
new CodePrimitiveExpression(child.SelectedImageIndex));
csc.Add(invokeExpr);
}
return codeObject;
}
History
- January - August 2001: Initial implementation of
PropertyTree
with VS.NET Beta 1 & 2
- November 2001: Released
PropertyTree
0.9, CodeProject article
- March 2002: Released
PropertyTree
1.0. Updated code for VS.NET
Final
- May 2002 - March 2003: Version 2.0 work (not much spare time...)
- February 2003: PropertyTree project setup on
SourceForge
- March 2003: CodeProject article updated for version 2.0