Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A designable PropertyTree for VS.NET

0.00/5 (No votes)
6 Mar 2003 1  
A TabControl-like options dialog based on a TreeView

PropertyTree (inside VS.NET)

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:

  1. Select the ".NET Framework Components" tab
  2. Click the "Browse..." button
  3. Navigate to and select the WRM.PropertyTree.dll assembly
  4. Make sure that the "PropertyTree" and "PropertyPane" controls are checked.
  5. 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:

// Create two root nodes in the PropertyTree

// 

// [Root]

//   - rootNode1

//   - rootNode2

//

PaneNode rootNode1 = propertyTree.PaneNodes.Add(new MyCustomPropertyPane());
PaneNode rootNode2 = propertyTree.PaneNodes.Add(new MyCustomPropertyPane());

// Add two child nodes to the first root node

//

// [Root]

//   - rootNode1

//     - child1

//     - child2

//   - rootNode2

PaneNode child1 = rootNode1.PaneNodes.Add(new MyOtherCustomPane());
PaneNode child2 = rootNode1.PaneNodes.Add(new MyOtherCustomPane());

// Now, remove the two child nodes from the first root node and add them

// to the second root node

//

// [Root]

//   - rootNode1

//   - rootNode2

//     - child1

//     - child2

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:

  1. PaneDeactivating (vetoable - involves currently selected PropertyPane)
  2. PaneActivating (vetoable - involves newly selected PropertyPane
  3. PaneDeactivated (involves currently selected PropertyPane)
  4. 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

AutoNavigate image

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:

// Collection built for PropertyTree specifically

//

public class PaneNodeCollection : Control.ControlCollection
{
...
}

public class PropertyTree : UserControl 
{
    ...
    // Override of Control.Controls

    //

    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) 
{
    // Make sure that this message is for the TreeView

    if(mPropertyTree.TreeView.Created  && 
        (m.HWnd == mPropertyTree.TreeView.Handle ||
        m.HWnd == mPropertyTree.Handle) )
    {    
        switch(m.Msg)
        {
            // If the user has pressed the left mouse button, select a node

            // in the TreeView if necessary

            // 

            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;

    //Add the pane to the PropertyTree

    pp.Text= name;
    PaneNode paneNode = null;

    if(parentNode == null)
        paneNode = mPropertyTree.PaneNodes.Add(pp);
    else
        paneNode = parentNode.PaneNodes.Add(pp);

    //Programatically select the pane we just added

    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:

  1. Determine Type of designed PropertyPane object
  2. Do not add ToolBox item if the Type is derived from SharedPropertyPane
  3. Otherwise, determine the display name and the bitmap to use for the ToolBox item
    1. Display name is class name without namespace
    2. Determine Bitmap to use
      1. Search for [Display name].bmp in resources
      2. Use default gray arrow bitmap
  4. Remove any existing ToolBox item that matches this one
  5. 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 /*ex*/)
    {
        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);

  // Create the call to PaneNodes.Add(...) for each PaneNode

  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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here