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

MS Office-like TaskPane Control

0.00/5 (No votes)
24 Apr 2007 2  
A .NET TaskPane control, with full design-time support.
Screenshot - TaskPane.jpg

Contents

Introduction

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.

Prerequisites

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.

Task Pane Design

The first step in creating the TaskPane control is to determine how to design its visual elements. The task pane has three basic elements:

  1. The caption bar
  2. The navigation buttons
  3. The content pane area
Screenshot - Scrolling.jpg

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:

  1. A toolstrip containing two Buttons - one back, one forward, a Label, a DropDownButton, and a Button for Close. As TaskPanePages 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.
  2. 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.
  3. 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:

Screenshot - DesignTimeOfficeXP_notated.jpg

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
Screenshot - DropDown.jpg
TaskPane Caption, Office XP Style
Screenshot - DropDownOfficeXPStyle.jpg

Designer Support

I have included both a Designer and an ActionList for both the TaskPane and the TaskPanePages. 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.

Designers

TaskPaneDesigner

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 TaskPanePages. Thus, I have the following overrides in the TaskPaneDesigner:

TaskPaneDesigner.cs

' No dragging of any tool is allowed on the taskpane itself.
' Tools can only be dragged onto the TaskPanePage panels themselves,
' which manage their own designers, so I don't need to control those.
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)

    ' Reject all drags
    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.

TaskPanePageDesigner

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 TaskPanePages to be parented to a TaskPane.

To this end, I have outfitted the TaskPanePageDesigner with two features:

  1. Custom 'adornments' which visually indicate the valid control area, and
  2. 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
    ' pnlNavButtons
    ' need to check parent control to see if pnlNavButtons
    ' contains point and reject a drag
    ' if it does
    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 TaskPanePages 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

Verbs & Action Lists (Smart Tags)

The TaskPane Verbs & ActionList

Screenshot - TaskPaneSmartTags.jpg

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 TaskPanePages, 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)

    'Add a new page to the collection
    pane.TaskPanePages.Add(page)

    ' Commit the change
    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 Verbs 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.

The TaskPanePage ActionList

Screenshot - TaskPanePageSmartTags.jpg

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 TaskPanePages, and to demonstrate a more complex Smart Tag.

An Interesting "Gotcha"

One thing I ran into while designing the TaskPanePages 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)
    ' What we're doing here is causing the designer to select the desired task pane page.
    ' This is so that the Smart Tag arrow will popup for the correct page.
    ' Without this little piece of code, it will continue to select the wrong page,
    ' until the user themselves selects the correct one by clicking on it.
    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.

ToDo List

There are several things as yet unimplemented. I have not implemented them for two reasons:

  1. 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
  2. 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.

About the Demo Project

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:

  1. MdiTabStrip - My absolutely favorite custom tabbed MDI interface written by crcrites
  2. 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 ' just to avoid null ref exceptions
        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 ' make sure it finds the NEXT match, not the current one
    End If
End Sub

TaskPane Implementation

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 TaskPanePages and ToolStripMenuItems 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.

Conclusion

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.

Resources

Here are several of the resources I used to learn how to provide full design-time support for custom controls:

  1. Simplify UI Development with Custom Designer Actions in Visual Studio
  2. Targeting Design-Time Events of User Controls
  3. Creating Custom Controls-Providing Design Time Support, Part 1
  4. Creating Custom Controls-Providing Design Time Support, Part 2

History

  • 4.26.2007 - Initial submission.

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