Table of Contents
In my previous article called XPlorerBar: A WPF Windows XP Style Explorer Bar Control, I provided a WPF implementation of the left side pane that was introduced in Windows XP's Explorer. As this article assumes that you are familiar with the XPlorerBar control, if you have not yet read it, please take a few minutes and read it.
Even if this control is easy to use and easily customizable, its handling still requires writing many lines of code. So, what if we added some design-time shortcuts to write this code for us? I'm sure you agree with me: It would be cool!!!
Therefore, in this second article dedicated to the XPlorerBar control, we will see how design-time features such as custom context-menus, custom adorners or custom initial default values can be added to the various elements of the WPF XPlorerBar control.
- Req 01 -
XPlorerBar, XPlorerSection and XPlorerItem features can easily be configured from design-time context-menus.
The requirements 02 to 05 apply to a selected XPlorerBar in design-time:
- Req 02 - The theme of the selected
XPlorerBar can be configured from the "Set theme" menu item of its design-time context-menu.
- Req 03 - The "Set theme" menu item displays (in a sub-menu) the list of all the built-in themes of the
XPlorerBar library. Remark: The current theme of the selected XPlorerBar is checked in the list.
- Req 04 - The "Allow multiple expands" property of the selected
XPlorerBar can be configured from the "Manage sections --> Allow multiple expands" menu item of its design-time context-menu. Remark: If the value of the property of this XPlorerBar is true then the "Allow multiple expands" menu item is checked.
- Req 05 - An
XPlorerSection can be added to the selected XPlorerBar by clicking the "Manage sections --> Add an XPlorerSection" menu item of its design-time context-menu. Remark: The new XPlorerSection is added at the bottom of the XPlorerBar.
The requirements 06 to 09 apply to a selected XPlorerSection in design-time:
- Req 06 - According to its current state, the selected
XPlorerSection can be expanded (or collapsed) by clicking the "Expand" (or "Collapse") menu item of its design-time context-menu.
- Req 07 - According to its current position, the selected
XPlorerSection can be moved up (or down) by clicking the "Manage section --> Move up" (or "Manage section --> Move down") menu item of its design-time context-menu. Remark: If the selected XPlorerSection is at the top of the XPlorerBar, the "Move up" menu item is disabled. Likewise, if the selected XPlorerSection is at the bottom of the XPlorerBar, the "Move down" menu item is disabled.
- Req 08 - The selected
XPlorerSection can be set as "Primary" by clicking the "Manage section --> IsPrimary" menu item of its design-time context-menu. Remark: if the selected XPlorerSection is set as "Primary", the "IsPrimary" menu item is checked.
- Req 09 - An
XPlorerItem can be added to the selected XPlorerSection by clicking the "Manage items --> Add an XPlorerItem" menu item of its design-time context-menu. Remark: The new XPlorerItem is added at the bottom of the XPlorerSection.
The requirement 10 applies to a selected XPlorerItem in design-time:
- Req 10 - According to its current position, the selected
XPlorerItem can be moved up (or down) by clicking the "Manage item --> Move up" (or "Manage item --> Move down") menu item of its design-time context-menu. Remark: if the selected XPlorerItem is at the top of the XPlorerSection, the "Move up" menu item is disabled. Likewise, if the selected XPlorerItem is at the bottom of the XPlorerSection, the "Move down" menu item is disabled.
Remark: for a better understanding, all the requirements are illustrated in the next part.
All the design-time features are located in the ZonaTools.XPlorerBar.VisualStudio.Design library. To enable the design-time support, make sure this library is located in the same folder as the ZonaTools.XPlorerBar library or in a "Design" folder under the ZonaTools.XPlorerBar library location.
Setting the theme
- Right-click on the XPlorerBar to be configured, and choose the new theme as shown in the figure below (Req. 02 and 03):
Setting the "Allow multiple expands" property
- Right-click on the XPlorerBar to be configured, and click the "Allow multiple expands" menu item as shown in the Figure below (Req. 04):
Adding a new XPlorerSection
- Right-click on the XPlorerBar to be configured, and click the "Add an XPlorerSection" menu item as shown in the figure below (Req. 05):
Expanding/Collapsing an XPlorerSection
- Right-click on the section to be collapsed (or expanded), and click the "Collapse" (or "Expand") menu item as shown in the figure below (Req. 06):
Moving up/down an XPlorerSection
- Right-click on the section to be moved up (or down), and click the "Move up" or ("Move down") menu item as shown in the Figure below (Req. 07):
Setting an XPlorerSection as "Primary"
- Right-click on the section to be configured, and click the "IsPrimary" menu item as shown in the figure below (Req. 08):
Adding a new XPlorerItem
- Right-click on the section to be configured, and click the "Add an XPlorerItem" menu item as shown in the figure below (Req. 09):
Moving up/down an XPlorerItem
- Right-click on the item to be moved up (or down) and click the "Move up" or ("Move down") menu item as shown in the figure below (Req. 10):
From the MSDN documentation: "The WPF designer (also called 'Cider') is based on a framework with an extensible architecture, which you can extend to create your own custom design experience".
The extensibility points in Cider (used to "create your own custom design experience") are all metadata based: That means that you use attributes to attach design-time features to your custom controls. Moreover, those attributes are added (for each type of object to extend) to a MetadataStore which is then loaded by the designer. This way, it will be as if you had added those attributes declaratively, but ONLY for design-time.
When you create a custom control, the code for your custom control and the metadata defining the design-time behavior of your control are factored into separate assemblies (in order to "physically" separate designer logic from runtime logic).
Thus, for a custom control stored in a library called myCustomControl.dll, there are three kinds of design-time assemblies, loaded in the following order:
- myCustomControl.Design.dll: Used to add design-time features common to both Visual Studio and Expression Blend,
- myCustomControl.VisualStudio.Design.dll: Used to add design-time features to Visual Studio only,
- myCustomControl.Expression.Design.dll: Used to add design-time features to Expression Blend only.
The metadata are factored into an entity called the MetadataStore, that attaches custom design-time features, such as custom adorners or custom context-menus, to specific types.
The MetadataStore is implemented as code-based attributes tables, that means that new features are declared in attributes tables which are then added to the MetadataStore in the Register() method of a class that implements IRegisterMetadata.
At this point, we have seen that attributes play a key role in the designer, but we still have not seen the kind of features we can implement with those attributes.
There are three main categories of features you can add to a custom control:
- Context menus - displayed by right-clicking on a selected element, they are used to add new actions to apply to the element (as the existing "View Code", "Order --> Bring to Front", "Delete" and "Properties" context-menu actions).
- Adorners - put on the design surface when a WPF element is selected, they are used to interact with the element and make updates to the XAML. For example, grid lines, anchor lines and the grab handles are all done with adorners.
- Default initializers - they are used to configure initial values for a new object created in the designer.
Those three categories share some basic services provided by the FeatureProvider abstract class. In fact, each time you have to extend design-time for a custom control, you have to derive from the FeatureProvider class or one of its child class.
Thus, the above categories are managed through the following classes:
PrimarySelectionContextMenuProvider: derive from this class to add the context menu items that appear when a control is selected on the design surface,
PrimarySelectionAdornerProvider: derive from this class to add adorners that appear when a control is selected on the design surface,
DefaultInitializer: derive from this class to configure initial values for a newly created control.
Before we dive into the design-time library code, let's have a look at the library project structure, as seen in Visual Studio's Solution Explorer:
The role of the library classes is explained below:
- [XPlorer*]ContextMenuProvider.cs: this class is used to add new context-menu items to the XPlorer* selected class. It also implements the actions to apply when a new context-menu item is selected.
- [XPlorer*]DefaultInitializer.cs: this class is used to define the initial values to apply to an XPlorer* class created in the designer.
- VisualStudioMetadata.cs: this class is used to add all the new design-time features (as attributes) to the
MetadataStore.
- ModelItemCollectionHelper.cs: this class is used to navigate in a
ModelItem collection.
Remark 1: XPlorer* represents XPlorerBar as well as XPlorerSection or XPlorerItem.
Remark 2: this project only provides new features through new context-menus items. The "Extra_XPlorerBarAdorner" folder is provided as an example, to show how custom adorners could be added to the XPlorerBar object.
- XPlorerBarAdornerProvider.cs: this class is used to add (as well as size and set its placement) the
DesignTimeGlyph adorner to the XPlorerBar class.
- DesignTimeGlyph.cs and .xaml: these classes are used to define the look and feel of the adorner.
Remark 3: in the following sections, all the examples are illustrated with the XPlorerBar object.
Adding context-menu items to an object as the XPlorerBar, is a three-part process:
- defining the new context-menu items,
- updating the status of the context-menu items,
- performing the actions associated to a selected context-menu item.
Defining the New Context-Menu Items
This part takes place in the constructor of the XPlorerBarContextMenuProvider class as shown below:
internal class XPlorerBarContextMenuProvider : PrimarySelectionContextMenuProvider
{
#region [ Fields ]
private MenuAction _allowMultipleExpandsMenuAction = new MenuAction(
"Allow multiple expands");
private MenuAction _addXPlorerSectionMenuAction = new MenuAction(
"Add an XPlorerSection");
...
#endregion
#region [ Constructor ]
public XPlorerBarContextMenuProvider()
{
...
#region 'Manage sections' menu
MenuGroup sectionsFlyoutGroup = new MenuGroup("SectionsGroup", "Manage sections");
sectionsFlyoutGroup.HasDropDown = true;
sectionsFlyoutGroup.Items.Add(_allowMultipleExpandsMenuAction);
_allowMultipleExpandsMenuAction.Checkable = true;
_allowMultipleExpandsMenuAction.Execute +=
new EventHandler<MenuActionEventArgs>(_allowMultipleExpands_Execute);
sectionsFlyoutGroup.Items.Add(_addXPlorerSectionMenuAction);
_addXPlorerSectionMenuAction.Execute +=
new EventHandler<MenuActionEventArgs>(_addXPlorerSection_Execute);
this.Items.Add(sectionsFlyoutGroup);
#endregion
UpdateItemStatus += new EventHandler<MenuActionEventArgs>
(XPlorerBarContextMenuProvider_UpdateItemStatus);
}
#endregion
...
}
There are two points of interest in the listing above:
- an event handler is added on the
Execute event of each of the MenuAction, which enables to perform the actions associated with the selected context-menu item,
- an event handler is added on the
UpdateItemStatus event of the XPlorerBarContextMenuProvider class, which gives the opportunity to update the context-menu items just before the context-menu is displayed.
Updating the Status of the Context-Menu Items
The listing below shows the way the status of the "Allow multiple expands" context-menu item is updated:
#region [ Updates the context menu ]
private void XPlorerBarContextMenuProvider_UpdateItemStatus(object sender,
MenuActionEventArgs e)
{
ModelItem selectedControl = e.Selection.PrimarySelection;
...
#region 'Manage sections' menu
_allowMultipleExpandsMenuAction.Enabled = true;
_allowMultipleExpandsMenuAction.Checkable = true;
_allowMultipleExpandsMenuAction.Checked = false;
ModelProperty allowMultipleExpandsProperty =
selectedControl.Properties[XPlorerBar.AllowMultipleExpandsProperty];
bool allowMultipleExpands = (bool)allowMultipleExpandsProperty.ComputedValue;
_allowMultipleExpandsMenuAction.Checked = allowMultipleExpands;
#endregion
}
#endregion
There are again two points of interest in the listing above:
- The type (
ModelItem) of the selected control. In the designer, the design environment interacts with controls through a programming interface called an editing model. The design environment uses the ModelItem type to communicate with the underlying model. All changes are made to the ModelItem wrappers, which then, affect the underlying model (see more on the editing model later).
- The two-part process to extract the value of a property from the selected control:
- First, the
selectedControl properties are accessed through its Properties collection. To retrieve a specific property (as a ModelProperty), use the name of the dependency property to be retrieved as an index in the Properties collection: selectedControl.Properties[<DPName>].
- Then, cast the
ComputedValue of the ModelProperty to the proper type.
Performing the Actions Associated to a Selected Context-Menu Item
As seen previously, these actions are triggered by handling the Execute event of the context-menu items.
The listing below shows how to set the value of a property of the currently selected control. The main point of interest here is the use of the SetValue method on the proper ModelProperty instance.
#region [ Sets the selected XPlorerBar 'AllowMultipleExpands' property ]
private void _allowMultipleExpands_Execute(object sender, MenuActionEventArgs e)
{
ModelItem selectedControl = e.Selection.PrimarySelection;
ModelProperty allowMultipleExpandsProperty =
selectedControl.Properties[XPlorerBar.AllowMultipleExpandsProperty];
bool selectedAllowMultipleExpands = ((MenuAction)sender).Checked;
allowMultipleExpandsProperty.SetValue(selectedAllowMultipleExpands);
}
#endregion
The next listing points out another use of the editing model of the designer with the ModelEditingScope class.
The ModelEditingScope represents a group of changes made to the editing store, that can be commited or aborted as a unit. When an editing scope is committed (by calling the Complete method on the ModelEditingScope instance), the editing store takes all changes that occurred in it and applies them to the model. Remark: to abort the changes, call the Revert method instead of the Complete method.
Another important thing to point out, is the use of the ModelFactory static class that enables the creation of instances of model items in the designer.
#region [ Adds a new XPlorerSection ]
private void _addXPlorerSection_Execute(object sender, MenuActionEventArgs e)
{
ModelItem selectedControl = e.Selection.PrimarySelection;
using (ModelEditingScope batchedChange =
selectedControl.BeginEdit("Adds a new XPlorerSection"))
{
ModelItem newItem = ModelFactory.CreateItem(e.Context, typeof(XPlorerSection),
CreateOptions.InitializeDefaults, new Object[0]);
selectedControl.Properties["Items"].Collection.Add(newItem);
batchedChange.Complete();
}
}
#endregion
Even if this part is not required to fulfill the functional requirements detailed at the beginning of this article, some code is provided in the "Extra_XPlorerBarAdorner" folder as an example of how adorners can be integrated into the design model.
To see the design-time adorner in action, uncomment the code below (located in the AddAdornerProviders method of the VisualStudioMetadata.cs file):
#region [ Adds specific adorner providers to the XPlorerBar elements ]
private void AddAdornerProviders(AttributeTableBuilder builder)
{
}
#endregion
And here is the result (notice the "smart tag"-like symbol that appears at the top-right of the selected XPlorerBar):
Programming such an adorner is a two-part process:
- First, the definition of the object to display: done in DesignTimeGlyph.xaml - for the look and feel of the adorner - and DesignTimeGlyph.cs - for the definition of the way the object is created (see more in the
InitializeComponent method).
- Second, the definition of the way the adorner will be displayed on the design surface: Done in the
XPlorerBarAdornerProvider class, the process is detailed below.
Except the creation of the DesignTimeGlyph object, done in the constructor, the main points of interest are located in the overridden Activate method shown below:
public class XPlorerBarAdornerProvider : PrimarySelectionAdornerProvider
{
#region [ Fields ]
private ModelItem m_adornedControlModel;
private DesignTimeGlyph m_designTimeGlyph;
private AdornerPanel m_xplorerBarAdornerPanel;
#endregion
#region [ Properties ]
public AdornerPanel Panel
{
get { ...}
}
#endregion
...
#region [ Creates and sets up the panel that holds the adorners ]
protected override void Activate(ModelItem item, DependencyObject view)
{
m_adornedControlModel = item;
m_adornedControlModel.PropertyChanged +=
new System.ComponentModel.PropertyChangedEventHandler(
m_adornedControlModel_PropertyChanged);
AdornerPanel adornerPanel = this.Panel;
AdornerPanel.SetHorizontalStretch(m_designTimeGlyph, AdornerStretch.Scale);
AdornerPanel.SetVerticalStretch(m_designTimeGlyph, AdornerStretch.Scale);
AdornerPlacementCollection placement = new AdornerPlacementCollection();
placement.SizeRelativeToAdornerDesiredWidth(1.0, 0.0);
placement.SizeRelativeToAdornerDesiredHeight(1.0, 0.0);
placement.PositionRelativeToAdornerHeight(-1.0, 0.0);
placement.PositionRelativeToContentWidth(1.0, -17.0);
AdornerPanel.SetPlacements(m_designTimeGlyph, placement);
base.Activate(item, view);
}
...
#endregion
}
The overrided Activate method is called when adorners are requested for the first time by the designer. Its main activities are:
- Save a
ModelItem representation of the adorned element and hooks into when it changes. This is useful when the adorner has to be updated each time the adorned element changed.
- Create an
AdornerPanel that will host one (or more) adorner(s).
- Set the size of each adorner included in the
AdornerPanel.
- Set the placement of each adorner included in the
AdornerPanel.
Defining default initial values for a newly created (in the designer) instance of a custom control is the simplest part of the design-time work, as shown in the listing below.
internal class XPlorerBarDefaultInitializer : DefaultInitializer
{
#region [ Initialization ]
public override void InitializeDefaults(ModelItem item)
{
using (ModelEditingScope batchedChange = item.BeginEdit("Creates an XPlorerBar"))
{
item.Properties["Width"].ClearValue();
item.Properties["Height"].ClearValue();
XPlorerSection newSection = new XPlorerSection();
item.Properties["Items"].Collection.Add(newSection);
batchedChange.Complete();
}
}
#endregion
}
As seen in the How it Works (Theory) part, the final piece of design-time features programming is adding all the new features to a MetadataStore (entity that stores information about the design-time behavior).
When the designer loads a custom control, it looks for a type in the corresponding design-time assembly that implements IRegisterMetadata, instantiates it and calls its Register method automatically.
As a consequence, all the new attributes adding to the MetadataStore are implemented in the Register method of the VisualStudioMetadata class, that inherits from IRegisterMetadata as shown in the listing below:
internal class VisualStudioMetadata : IRegisterMetadata
{
#region [ Registers attributes to the metadata store ]
public void Register()
{
AttributeTableBuilder builder = new AttributeTableBuilder();
AddDefaultInitializerClasses(builder);
AddContextMenuProviders(builder);
AddAdornerProviders(builder);
MetadataStore.AddAttributeTable(builder.CreateTable());
}
#endregion
...
}
The listing above shows how the new design-time attributes are collected through an instance of AttributeTableBuilder that is then added to the MetadataStore static class.
Finally, let's see how the design-time features are "converted" into attributes and added to the AttributeTableBuilder instance. As the process is identical for context-menus providers, adorners providers and default initializers, let's see the details of the AddContextMenuProviders method only, shown in the listing below:
#region [ Adds context menu providers to the XPlorerBar elements ]
private void AddContextMenuProviders(AttributeTableBuilder builder)
{
builder.AddCustomAttributes(typeof(XPlorerBar),
new FeatureAttribute(typeof(XPlorerBarContextMenuProvider)));
builder.AddCustomAttributes(typeof(XPlorerSection),
new FeatureAttribute(typeof(XPlorerSectionContextMenuProvider)));
builder.AddCustomAttributes(typeof(XPlorerItem),
new FeatureAttribute(typeof(XPlorerItemContextMenuProvider)));
}
#endregion
The most important point to bear in mind here is that each of the providers implemented is added to the AttributeTableBuilder instance as a new FeatureAttribute associated to the proper custom type.
To learn a lot more about the designer, check out these links:
The project was developed with the following tools:
As the reader of this article, your opinion is the most valuable contribution to make this article better. So, please let me know what you think.
I definitely want to hear what the community thinks.
- improve the design-time interaction by adding custom panels to configure the various elements,
- enable the setting of text and icon for XPlorerItem and XPlorerSection headers,
- add Blend design-time support.
- November 5, 2008 - v1.0
- December 8, 2008 - v1.1
- Added the Blend theme to the built-in themes of the library.
- Added a completely new class for theme management which allows to apply themes to any
FrameworkElement object. This class replaces the previous ThemeManager class.
- Removed the now useless
ThemeDictionary class.
- Updated the
XPlorerBar class so that an XPlorerBar can now be built dynamically with bindings to a data source.
- Renamed the
ExpandableDecorator class in XPandableDecorator.
- December xx, 2008 - v1.2
- Added Visual Studio 2008 design-time support.
This article, along with any associated source code and files, is licensed under the Code Project Open License (CPOL).