Contents
This article describes how to create a TaskPane
control that mimics the TaskPane seen in Microsoft Office XP and 2003 (I don't have a copy of Office 2007, so I have no idea if this interface has been retained). I will describe the implementation of the control, but I will pay special attention to the design-time features included in the control, because that was the more challenging aspect of this project.
I have written this component in VB.NET due to project constraints. If there is enough interest, I will port it to my stronger language, C#.
Enough introduction - let's dive in.
This project uses the Ascend.NET control suite to create the gradient panels. Yes, I could have created these myself, but I wanted to discover how quickly I could create a fully functional, fully featured control using available tools. You can download the Ascend.NET control suite here, or you can download the demo project, which contains the relevant DLLs. Be warned, the Ascend.NET suite installs itself into your Visual Studio toolbox, so if this is an undesirable behavior, simply use the DLLs in my demo project.
The first step in creating the TaskPane
control is to determine how to design its visual elements. The task pane has three basic elements:
- The caption bar
- The navigation buttons
- The content pane area
|
|
Note the way the scrollbars appear when the content pane is smaller to the available control area. This is a slight departure from the true TaskPane behavior, which produces a scroll thumb at the top and bottom of the content area, in the width of the content area. I figured unless anyone really wants that behavior, it would be okay to leave it out.
The simplest way I could think of to present these three elements was with the following controls:
- A toolstrip containing two
Button
s - one back, one forward, a Label
, a DropDownButton
, and a Button
for Close. As TaskPanePage
s are added to the control, a new ToolStripMenuItem
is added to the DropDownButton
. When the SelectedIndex
or SelectedTab
changes, the appropriate caption and caption image are set. The CaptionStyle
can be changed by the corresponding property in the designer.
- For the Office 2003 design, a small Ascend.NET Gradient Panel docked to the top of the content area. The
NavigationStyle
property determines whether the navigation buttons appear on the caption area or in the content pane.
- A special panel called a
TaskPanePage
, inherited from the Ascend.NET Gradient Panel, with additional data properties and a custom designer. These pages are available in the designer. The most pertinent properties can be set using the SmartTag
panel.
The following figure demonstrates these three basic features:
I wanted the TaskPane
to support both the Office XP style, which contains the navigation buttons inside the caption area, and the Office 2003 style, which places the navigation buttons inside the content pane.
TaskPane Caption, Office 2003 Style
|
TaskPane Caption, Office XP Style
|
I have included both a Designer and an ActionList
for both the TaskPane
and the TaskPanePage
s. These objects provide full design-time support for the control - in fact, in the designer, it's hard to tell you're even designing the control - the design time behavior is exactly the same as the runtime behavior.
In order to allow the TaskPane
to behave during design-time the way it would during run-time, I had to make the designer aware of the controls contained in my custom control. This is achieved by creating a designer that inherits from ParentControlDesigner
, rather than simply ControlDesigner
- ParentControlDesigner
allows you to specify parenting rules for controls. For example, there is no need to allow controls to be dragged onto the TaskPane
itself - controls can only be added to TaskPanePage
s. Thus, I have the following overrides in the TaskPaneDesigner
:
TaskPaneDesigner.cs
Public Overrides Function CanParent(ByVal control As _
System.Windows.Forms.Control) As Boolean
Return False
End Function
Public Overrides Function CanParent(ByVal controlDesigner _
As ControlDesigner) As Boolean
Return False
End Function
Protected Overrides Sub OnDragOver(ByVal de As _
System.Windows.Forms.DragEventArgs)
MyBase.OnDragOver(de)
de.Effect = DragDropEffects.None
End Sub
Without the OnDragOver
override, you would not get the 'Disallow' symbol over the control - all that would happen is that any control you attempt to drag onto the TaskPane
would disappear.
A more interesting aspect of this designer, however, is how it allows the designer to be aware of child controls, and allow the developer to interact with them as though it were runtime. In order for this to work, the designer needs to know there's a control at a MouseClick
point. To provide this information, we must override the GetHitTest
method for our designer:
Protected Overrides Function GetHitTest(ByVal point As System.Drawing.Point) As Boolean
Dim pane As TaskPane = TryCast(Me.Control, TaskPane)
If pane Is Nothing Then Return False
If pane.btnNavBack.Enabled AndAlso _
pane.btnNavBack.ClientRectangle.Contains(pane.btnNavBack.PointToClient(point)) Then
Return True
End If
If pane.btnNavForward.Enabled AndAlso _
pane.btnNavForward.ClientRectangle.Contains(pane.btnNavForward.PointToClient(point)) Then
Return True
End If
If pane.btnNavHome.Enabled AndAlso _
pane.btnNavHome.ClientRectangle.Contains(pane.btnNavHome.PointToClient(point)) Then
Return True
End If
If pane.btnToolBack.Enabled AndAlso _
pane.btnToolBack.Bounds.Contains(pane.toolStripToolsButtons.PointToClient(point)) Then
Return True
End If
If pane.btnToolForward.Enabled AndAlso _
pane.btnToolForward.Bounds.Contains(pane.toolStripToolsButtons.PointToClient(point)) Then
Return True
End If
If pane.btnToolTaskTools.Enabled AndAlso _
pane.btnToolTaskTools.Bounds.Contains(pane.toolStripToolsButtons.PointToClient(point)) Then
Return True
End If
If pane.btnClose.Enabled AndAlso _
pane.btnClose.Bounds.Contains(pane.toolStripToolsButtons.PointToClient(point)) Then
Return True
End If
Return False
End Function
You'll notice I'm using direct references to the objects in the TaskPane
. These objects have their protection level set at Protected Friend
(protected internal
, in C#) so that they can be visible to the designer, but not to consumers of the control.
For the TaskPanePage
object, I wanted to provide both a rich design-time experience and an indicator of which page is being designed, along with the valid control-drop area. Obviously, we don't want users dropping controls over the Navigation buttons when in Office 2003 mode. Finally, we only want to allow TaskPanePage
s to be parented to a TaskPane
.
To this end, I have outfitted the TaskPanePageDesigner
with two features:
- Custom 'adornments' which visually indicate the valid control area, and
- Control over the drag and hit tests to prevent controls from being dragged over the navigation buttons. Obviously, nothing prevents the developer from doing this at runtime, but then at least, they'll be very sure they actually want to.
These are accomplished with the following methods in the designer:
Public Overrides Function CanBeParentedTo(ByVal parentDesigner _
As IDesigner) As Boolean
Return TypeOf (parentDesigner) Is TaskPaneDesigner
End Function
Protected Overrides Sub OnPaintAdornments(ByVal pe As _
System.Windows.Forms.PaintEventArgs)
MyBase.OnPaintAdornments(pe)
PaintDesignerInfo(pe.Graphics)
End Sub
Protected Overrides Sub OnDragOver(ByVal de As System.Windows.Forms.DragEventArgs)
MyBase.OnDragOver(de)
If GetHitTest(New Point(de.X, de.Y)) Then
de.Effect = DragDropEffects.None
End If
End Sub
Protected Overrides Function GetHitTest(ByVal point As _
System.Drawing.Point) As Boolean
Dim prnt As TaskPane = TryCast(Me.Control.Parent, TaskPane)
If prnt Is Nothing Then Return False
If prnt.pnlNavButtons.ClientRectangle.Contains(
prnt.pnlNavButtons.PointToClient(point)) Then
Return True
End If
Return False
End Function
The PaintDesignerInfo
method creates a hatch brush and outlines the border of the TaskPanePage
. For additional details on how that method does painting, see the source code.
And, I have again overridden the CanParent
method:
Public Overrides Function CanBeParentedTo(ByVal parentDesigner As IDesigner) As Boolean
Return TypeOf (parentDesigner) Is TaskPaneDesigner
End Function
Nor do we want the TaskPanePage
s to be movable at design-time: these controls must always be docked to fill the content area. This leads to the following override:
Protected Overrides ReadOnly Property EnableDragRect() As Boolean
Get
Return False
End Get
End Property
Public Overrides ReadOnly Property SelectionRules() As SelectionRules
Get
Return Windows.Forms.Design.SelectionRules.Locked
End Get
End Property
One nice feature of many .NET controls is the ability to have a Smart Tag allowing you to quickly add a new item to a control's collection - for example, the TabControl
has a Smart Tag allowing to add a new tab page. I thought it would be nice to add a similar feature for the TaskPane
to add new TaskPanePage
s, and it is surprisingly easy to do. In the TaskPaneDesigner
, simply add the following method:
Public Overrides ReadOnly Property Verbs() As _
System.ComponentModel.Design.DesignerVerbCollection
Get
Dim vbs As New DesignerVerbCollection
vbs.Add(New DesignerVerb("Add Task Page", _
New EventHandler(AddressOf handleAddPage)))
Return vbs
End Get
End Property
Private Sub handleAddPage(ByVal sender As Object, ByVal e As EventArgs)
Dim pane As TaskPane = CType(Me.Control, TaskPane)
Dim h As IDesignerHost = CType(GetService(GetType(IDesignerHost)), IDesignerHost)
Dim c As IComponentChangeService = _
CType(GetService(GetType(IComponentChangeService)), IComponentChangeService)
Dim dt As DesignerTransaction = h.CreateTransaction("Add Task Page")
Dim page As TaskPanePage = _
CType(h.CreateComponent(GetType(TaskPanePage)), TaskPanePage)
c.OnComponentChanging(pane, Nothing)
pane.TaskPanePages.Add(page)
c.OnComponentChanged(pane, Nothing, Nothing, Nothing)
dt.Commit()
End Sub
The component transaction stuff allows the designer to register Undo/Redo information.
I also thought it would be convenient to allow the user to set the docking behavior of the TaskPane
in the designer. This required adding an ActionList
. The actual ActionList
object itself has more functionality to it, but the essential part is this:
Public Overrides Function GetSortedActionItems() As DesignerActionItemCollection
Dim items As DesignerActionItemCollection = New DesignerActionItemCollection
items.Add(New DesignerActionMethodItem(Me, "handleAddPage", _
"Add TaskPane Page", "Actions", _
"Adds a new TaskPanePage to the TaskPane", False))
items.Add(New DesignerActionPropertyItem("Dock", "Dock", _
"Appearance", "Docking position of the TaskPane"))
Return items
End Function
This is where the actual items that show up on the Smart Tag are added. You'll notice I've added that "handleAddPage
" method again - this is because if you add both Verb
s and an ActionList
to a designer, the ActionList
will cause the verbs to no longer be used on the Smart Tag (the verbs will remain on the context menu, but they won't be placed on the Smart Tag). Thus, the Add Page item must be added to the action list as well.
This action list is built much the same way as the TaskPane
action list, so I will let you look at the code for the specifics. I simply wanted to show which items are available through the designer for the TaskPanePage
s, and to demonstrate a more complex Smart Tag.
An Interesting "Gotcha"
One thing I ran into while designing the TaskPanePage
s was rather annoying, until I found an interesting solution to the problem: which TaskPanePage
is currently selected in the designer.
In the event handler for switching between pages (using either the DropDownList
or the navigation buttons), I call BringToFront()
which works to bring the page to the front of the Z-order; however, it does not change which control the designer is selecting. Thus, if you have a Smart Tag open for a TaskPanePage
, and then switch pages, the old page remains selected. Clicking in the content area selects the new page in the designer, but I wanted a way to do this programmatically. I had all but given up until I found the following solution on the MSDN forums:
Private Sub SelectedPageChanged(ByVal sender As Object, ByVal e As EventArgs)
Dim dHost As IDesignerHost = CType(GetService(GetType(IDesignerHost)), IDesignerHost)
Dim selectionService As ISelectionService = _
CType(dHost.GetService(GetType(ISelectionService)), ISelectionService)
selectionService.SetSelectedComponents(New Object() {Me.Pane.SelectedPage})
End Sub
By adding an event handler to the TaskPane
's SelectedIndexChanged
event in the Initialize()
method of the TaskPaneDesigner
, I am able to make the designer aware of changes to the selected page. I, then, call out to the ISelectionService
of the designer in order to tell it which control to select, thus solving my problem.
There are several things as yet unimplemented. I have not implemented them for two reasons:
- They are not of immediate necessity to this project (the application layer is already written, this control is a drop-in replacement of an existing control so I already know its use cases), and
- I'm not entirely sure what should be implemented.
I have implemented the obvious events:
TaskPaneCloseClick
SelectedIndexChanging
(for canceling the change)
SelectedIndexChanged
I think navigation events for forward, back, and home would be useful. I also think that it might be useful to add separators between the dropdown menu items to create groupings like the Office TaskPane does.
But what other events and properties are really necessary? Feedback is appreciated.
The demo project includes several things not directly relevant to the TaskPane
control, but I felt it would be helpful to provide a complete example rather than a stub that merely contains the control and nothing else. To that end, the demo contains a couple of my favorite controls here on CodeProject, and I would like to mention them here:
- MdiTabStrip - My absolutely favorite custom tabbed MDI interface written by crcrites
- Windows XP-Style Explorer Bar - An interesting themed control that seems very appropriate to a
TaskPane
interface, written by Mathew Hall
For the editor interface, I have chosen to use a RichTextBox
in a subform I call DocumentForm
. The search box in the TaskPane
allows you to actually search text in the currently selected tabbed document, and any text found will be highlighted appropriately:
TaskPaneExampleForm.vb:
Private ReadOnly Property SelectedDocument() As DocumentForm
Get
Dim frm As DocumentForm = Nothing
Dim tab As MdiTabStrip.MdiTab = MdiTabStrip1.ActiveTab
If tab IsNot Nothing Then
frm = TryCast(tab.Form, DocumentForm)
End If
If frm Is Nothing Then
frm = New DocumentForm
End If
Return frm
End Get
End Property
...
Private Sub btnSearch_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnSearch.Click
Me.SelectedDocument.FindText(txtSearch.Text)
End Sub
File DocumentForm.vb:
Public Sub FindText(ByVal argText As String)
If argText.Length = 0 Then Return
rtbDocument.Find(argText, m_CurrentStart, RichTextBoxFinds.None)
End Sub
Private Sub rtbDocument_SelectionChanged(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles rtbDocument.SelectionChanged
m_CurrentStart = rtbDocument.SelectionStart
If rtbDocument.SelectionLength > 0 Then
m_CurrentStart += 1
End If
End Sub
The implementation of the TaskPane
is relatively straightforward, which is why I haven't felt inclined to focus much on it.
The TaskPane
contains an instance of a TaskPanePageCollection
(which inherits CollectionBase
). I handle events for controls being added, removed, and set in the collection, and add, remove, and set the corresponding TaskPanePage
s and ToolStripMenuItem
s for the caption.
I considered inheriting Control.ControlCollection
and overriding CreateControlInstance()
in the TaskPane
, but decided against that approach because of the ToolStrip and the gradient panel used for the navigation buttons.
When the SelectedIndex
of the TaskPane
changes, the appropriate TaskPanePage
is shown and its corresponding ToolStripMenuItem
is set as the current caption and image. It really isn't much more complex than that.
Well, I think that's about it. If I haven't fully explained anything, or you have bug fixes or ideas on what could be added to the control, let me know.
Here are several of the resources I used to learn how to provide full design-time support for custom controls:
- Simplify UI Development with Custom Designer Actions in Visual Studio
- Targeting Design-Time Events of User Controls
- Creating Custom Controls-Providing Design Time Support, Part 1
- Creating Custom Controls-Providing Design Time Support, Part 2
- 4.26.2007 - Initial submission.