
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 PropertyPanes
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 PropertyPanes |
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 TabPages in a
TabControl. |
Custom PropertyPanes |
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 PropertyPanes |
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 PropertyPanes in the
PropertyTree, and the controls on each of the
PropertyPanes.
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 PropertyPanes 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
PropertyPanes. If you've got a fair amount of controls on your
anonymous PropertyPanes, 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 PropertyPanes 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 PropertyPanes can be created and
designed in the same way as custom PropertyPanes, but they cannot
be added to a PropertyTree at design-time. This is because shared
PropertyPanes - 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
PropertyPanes 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 PropertyPanes 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 PropertyPanes from a PropertyTree.
Working with PropertyPanes
Regardless of how you design your PropertyPanes (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 PropertyPanes 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 PropertyPanes 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 PropertyPanes 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
PropertyPanes, these PropertyTree events are where you
must handle the selection change process.
When you are working with custom or shared PropertyPanes, 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 PropertyPanes
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 TabPages property. This works well for TabControl
because its collecion of child controls is homogenous - they are all
TabPages. 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 PropertyPanes, it also
contains a TreeView, some Labels, 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 PropertyPanes' 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 PropertyPanes 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
PropertyPanes.
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
PropertyPanes 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 PropertyPanes 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