Displaying a Directory Search Custom Task Pane from the 2007 Office Ribbon

Summary: Build a project that combines several 2007 Microsoft Office and Microsoft Visual Studio technologies. These include creating a custom Office Fluent Ribbon, custom task pane, working with Open XML Format files, and using Microsoft Visual Studio 2005 Tools for Office. (12 printed pages)

Frank Rice, Microsoft Corporation

April 2008

Applies to: Microsoft Office Word 2007, Microsoft Visual Studio 2008, Microsoft Visual Studio 2005 Tools for the 2007 Microsoft Office System, Second Edition

Contents

  • Overview

  • Creating the Word Search Add-in Project

  • Adding a Custom Ribbon to the Project

  • Creating the Custom Task Pane

  • Adding File Search Functionality to the Task Pane

  • Testing the Project

  • Conclusion

  • Additional Resources

Overview

In this article, you create a custom word search Add-in in Microsoft Office Word 2007 that allows you to specify a search term and directory path, and then searches for that word in the files and sub-directories of the search path.

The project that I'll demonstrate in this article features a number of technologies. First, you create a custom Microsoft Office Fluent Ribbon (hereafter known as the Ribbon) tab and button in a Word 2007 document. Clicking this button displays a custom task pane that you also create. The task pane contains custom controls that allow you to specify parameters and start the custom search.

Each of the files searched is an Open XML Format Word 2007 (.docx) document which is essentially a zipped package of parts that combine to make a document. The package is opened by using the Open XML application programming interface (API), and the document part is scanned for the occurrence of the search term. If the file is found to contain the search term, the path and filename are displayed in the Word document containing the task pane. The entire project is created in Microsoft Visual Studio 2005 Tools for Office Second Edition (hereafter known as VSTO SE).

The result of the project is to create a tab in Word 2007 as shown in Figure 1.

NoteNote

You can download VSTO SE for free from the Microsoft Developer Network. The VSTO SE can be used in both Microsoft Visual Studio 2005 and Microsoft Visual Studio 2008.

Creating the Word Search Add-in Project

In the following steps, you create a Word 2007 Add-in in Microsoft Visual Studio.

To create the Word Search Add-in

  1. Start Microsoft Visual Studio 2008 (or Microsoft Visual Studio 2005).

  2. On the File menu, click New Project.

  3. (Visual Basic only) In the New Project dialog box, in the Project types pane, expand Visual Basic, Office, and then click 2007.

  4. (C# only) In the New Project dialog box, in the Project types pane, expand Other languages, Visual C#, Office, and then click 2007.

  5. In the Templates pane, select the Word 2007 Add-in icon.

  6. Specify the name of the project as WordSearch, and then click OK.

  7. Next, you need to add a reference to the WindowsBase library to the project. This library of methods is used to open and manipulate the Open XML Format files later in this article. On the Project menu, click Show All Files.

  8. In the Solution Explorer, right-click the WordSearch node and then click Add Reference.

  9. In the Add Reference dialog box, on the .NET tab, scroll-down, select WindowsBase, and then click Add. Notice that the WindowsBase reference is added to the References folder.

  10. Although you have not built the custom task pane yet, you can still add code that displays or hides the task pane when the button on the Ribbon is clicked. In the Solution Explorer, right-click the ThisAddIn.vb (ThisAddIn.cs) node and click View Code.

  11. Copy and paste or type the following code into the code window just after the Public Class ThisAddIn (public partial class ThisAddIn) statement.

    Private ctpSearch As Microsoft.Office.Tools.CustomTaskPane
    
    Public Sub AddSearchTaskPane()
       ctpSearch = Me.CustomTaskPanes.Add(New wordSearchControl(), _
          "File Search Task Pane")
       ctpSearch.DockPosition = _
          Microsoft.Office.Core.MsoCTPDockPosition.msoCTPDockPositionRight
            ctpSearch.Visible = True
    End Sub
    
private Microsoft.Office.Tools.CustomTaskPane ctpSearch;

public void AddSearchTaskPane()
{
   ctpSearch = this.CustomTaskPanes.Add(new wordSearchControl(), "File Search Task Pane");
   ctpSearch.DockPosition = Microsoft.Office.Core.MsoCTPDockPosition.msoCTPDockPositionRight;
   ctpSearch.Visible = true;
}

As its name implies, this procedure is used to display the task pane. First, you set a reference to a task pane object. In the AddSearchTaskPane procedure, the wordSearchControl custom task pane is added to the collection of task panes and assigned to the variable you defined earlier. The title of the task pane is set as File Search Task Pane. The docked position of the pane is set and the task pane is made visible.

Next, add the procedure that hides the task pane when the button is clicked a second time.

Public Sub RemoveSearchTaskPane()
   If Me.CustomTaskPanes.Count > 0 Then
      Me.CustomTaskPanes.Remove(ctpSearch)
   End If
End Sub
public void RemoveSearchTaskPane()
{
   if ((this.CustomTaskPanes.Count > 0))
   {
      this.CustomTaskPanes.Remove(ctpSearch);
   }
}

In this procedure, the count of open task panes is checked and if any task panes are displayed, the ctpSearch task pane is hidden, if it is open.

Finally, add the following procedure, if it doesn't already exist. This procedure returns a reference to a new Ribbon object to Microsoft Office when initialized.

Protected Overrides Function CreateRibbonExtensibilityObject() As Microsoft.Office.Core.IRibbonExtensibility
   Return New Ribbon()
End Function
protected override Microsoft.Office.Core.IRibbonExtensibility CreateRibbonExtensibilityObject()
{
   return new Ribbon();
}

Adding a Custom Ribbon to the Project

In the following steps, you create the custom tab containing a button control. This tab is added to the existing Ribbon in Word 2007 when the Add-in is loaded.

To create the custom ribbon

  1. In the Solution Explorer, right-click the WordSearch node, point to Add, and then click Add New Item.

  2. In the Add New Item dialog box, in the Templates pane, select Ribbon (Visual Designer), and then click Add. The Ribbon1.vb (Ribbon1.cs)node is added to the Solution Explorer and the Ribbon Designer is displayed.

  3. In the Ribbon Designer, click the TabAddIns tab.

  4. In the Properties pane, change the Label property to Word Search. Notice that the title of the tab is updated in the Ribbon Designer.

    You see how easy it is to change the Ribbon properties in the Visual Designer. Now in the following steps, you do the same thing but this time directly in the XML file that defines the Ribbon.

  5. Right-click the Ribbon Designer and then click Export Ribbon to XML. Notice that Ribbon.xml and Ribbon.vb (Ribbon.cs) files are added to the Solution Explorer.

  6. Double-click the Ribbon.xml file to display the code window. The XML you see defines the Ribbon thus far. For example, the label attribute for the <tab> element is set to Word Search just as you manually set that property earlier.

  7. To define the other components of the Ribbon, replace the XML with the following code.

    <?xml version="1.0" encoding="UTF-8"?>
    <customUI onLoad="Ribbon_Load" xmlns="http://schemas.microsoft.com/office/2006/01/customui">
      <ribbon>
        <tabs>
          <tab id="searchTab" label="Word Search"
               insertAfterMso="TabHome">
            <group id="searchGroup" label="Search">
              <button id="btnTaskPane"
                 label="Display Search Pane"
                 onAction="btnTaskPane_Click" />
            </group>
          </tab>
        </tabs>
      </ribbon>
    </customUI>
    

    In general, elements define controls on the Ribbon such as tabs, groups, buttons, or combo boxes, and attributes define properties of those controls such as a control's label or whether it is visible or hidden.

    Looking at the code, notice that in addition to the <tab> element, there is a <group> element and <button> element. A group is used as a container for controls with similar functions. For example, the Font group on the Home tab in Word 2007. The button is used to initialize some event such as displaying the search task pane.

    Also notice in the code the insertAfterMso attribute of the <tab> element. Any attribute that ends in Mso specifies functionality that is built into Microsoft Office. In this instance, the insertAfterMso attribute tells Word 2007 to insert the tab you create after the built-in Home tab.

    Next, you use the label attribute to add a caption to the button. In addition, the button has an onAction attribute that points to the procedure that is executed when you click the button. These procedures are also known as callback procedures. When the button is clicked, the onAction attribute calls back to Microsoft Office, which then executes the specified procedure.

    The net result of the XML is to create a tab in the Word 2007 Ribbon that looks similar to that seen in Figure 1.

    Figure 1. The Word Search tab in the Word 2007 Ribbon

    The Word Search tab in the Word 2007 Ribbon

    When you created the Ribbon Designer, notice that the Ribbon.vb (Ribbon.cs) file was also created for you. This file contains the callback and other procedures that you need to make the Ribbon functional.

  8. Open the Ribbon.vb (Ribbon.cs) file by right-clicking it in the Solution Explorer and clicking View Code.

    NoteNote

    As you may notice, there are several lines of comments in the file that you may want to delete before continuing.

    When you add the Ribbon to your project, a Ribbon object class and a few procedures are already available in the Ribbon's code-behind file.

    Public Class Ribbon Implements Office.IRibbonExtensibility
    
public class Ribbon : Office.IRibbonExtensibility

Notice that the Ribbon class implements the Office.IRibbonExtensibility interface. This interface defines one method named GetCustomUI.

Public Function GetCustomUI(ByVal ribbonID As String) As String Implements Office.IRibbonExtensibility.GetCustomUI
   Return GetResourceText("WordSearch.Ribbon.xml")
End Function
public string GetCustomUI(string ribbonID)
{
   return GetResourceText("WordSearchCS.Ribbon.xml");
}

When the Ribbon is loaded by Microsoft Office, the GetCustomUI method is called and returns the XML that defines the Ribbon components to Office.

Now you need to add the callback procedures to the class that give the Ribbon its functionality.

  1. In the Ribbon Callbacks block, add the following procedure.

    Private WordSearchControlExists As Boolean
    
    Public Sub btnTaskPane_Click(ByVal control As Office.IRibbonControl)
       If Not WordSearchControlExists Then
          Globals.ThisAddIn.AddSearchTaskPane()
       Else
          Globals.ThisAddIn.RemoveSearchTaskPane()
       End If
       WordSearchControlExists = Not WordSearchControlExists
    End Sub
    
bool WordSearchControlExists = false;

public void btnTaskPane_Click(Office.IRibbonControl control)
{
   if (!WordSearchControlExists)
   {
      Globals.ThisAddIn.AddSearchTaskPane();
   }
   else
   {
      Globals.ThisAddIn.RemoveSearchTaskPane();
   }
   WordSearchControlExists = !WordSearchControlExists;
}

This callback procedure is called when the button that you added to the Ribbon earlier is clicked. As stated earlier, its purpose is to display or hide the custom task pane. It does this by checking state of the WordSearchControlExistsBoolean variable. Initially, by default, this variable is set to False. When the procedure is called, Not WordSearchControlExists (!WordSearchControlExists) equates to True so the AddSearchTaskPane method of the ThisAddIn class is called. This causes the task pane to be displayed. The WordSearchControlExists variable is then set to True. When the button is clicked again, Not WordSearchControlExists (!WordSearchControlExists) now equals False, the RemoveSearchTaskPane procedure is called, and the task pane is hidden.

Creating the Custom Task Pane

In the following steps, you create the search task pane and populate it with labels, textboxes, and a button. The textboxes allow you to specify a directory path and term to search for. The button initiates the search.

To create the custom task pane

  1. In the Solution Explorer, right-click the WordSearch node, point to Add, and then click Add New Item.

  2. In the Add New Item dialog box, select the User Control, name it wordSearchControl.vb (wordSearchControl.cs) and click Add.

    Next, add the task pane controls.

  3. On the View menu, click Toolbox.

    From the toolbox, add the controls specified in Table 1 to the wordSearchControl Designer and set the properties as shown. The design should look similar to Figure 2.

Table 1. Add these controls to the wordSearchControl control

Control Type

Property

Setting

Label

Text

Type a search path:

TextBox

Name

txtPath

Label

Text

Type a search term:

TextBox

Name

txtSearchTerm

Button

Name

btnSearch

Text

Search

Figure 2. The wordSearchControl control

The wordSearchControl control

Now add the code to the button to make it functional.

  1. In the Solution Explorer, right-click the wordSearchControl.vb (wordSearchControl.cs) node, and click View Code.

  2. Now, above the Public Class wordSearchControl (public partial class wordSearchControl : UserControl) declaration, add the following namespaces to the existing declarations. These are containers for the various objects and methods used in the project.

    Imports System.Xml
    Imports System.IO
    Imports System.IO.Packaging
    Imports System.Collections.Generic
    Imports System.Windows.Forms
    
using System.IO;
using System.IO.Packaging;
using System.Xml;
  1. Next, add the following code after the Public Class wordSearchControl (public partial class wordSearchControl : UserControl) statement.

    Private Const docType As String = "*.docx"
    
    Private Sub btnSearch_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnSearch.Click
       Dim sDir As String = txtPath.Text
       Call DirSearch(sDir)
    
       MessageBox.Show("Search complete.")
    End Sub
    
string docType = "*.docx";

private void btnSearch_Click(object sender, EventArgs e)
{
   string sDir = txtPath.Text;
   DirSearch(sDir);
   MessageBox.Show("Search complete.");
}

First, the document-type of the search files is defined. Placing the declaration at the top of the class makes it easier to change the type of document, if desired. The btnSearch_Click procedure is called when you click the Search button on the task pane. First, it assigns the value representing the search path from the txtPath textbox to a variable. This variable is then passed to the DirSearch method. Finally, a message box that signals that the search has finished is added.

Adding File Search Functionality to the Task Pane

In the following steps, you modify the code behind the custom task pane controls to give them search functionality.

To add search capability to the task pane controls

  • In the wordSearchControl.vb (wordSearchControl.cs) code window, add the following statements.

    Private alreadyChecked As Boolean = False
    
    Private Sub DirSearch(ByVal sDir As String)
       Dim d As String
       Dim f As String
       Dim searchTerm As String = txtSearchTerm.Text
       Try
          If Not alreadyChecked Then
             'Check all of the files in the path directory first.
             For Each f In Directory.GetFiles(sDir, docType)
                Call GetToDocPart(f, searchTerm)
             Next
                    alreadyChecked = True
          End If
             'If there are sub-directories, check those files next.
             For Each d In Directory.GetDirectories(sDir)
                For Each f In Directory.GetFiles(d, docType)
                   Call GetToDocPart(f, searchTerm)
                Next
                DirSearch(d)
             Next
       Catch
          MessageBox.Show("There was a problem with the file " & f)
       End Try
    End Sub
    
private bool alreadyChecked = false;

private void DirSearch(string sDir) {
   string badFile = "";    
   string searchTerm = txtSearchTerm.Text;
   try {
      if (!alreadyChecked) {
         // Check all of the files in the path directory first.
         foreach (string f in Directory.GetFiles(sDir, docType)) {
                    GetToDocPart(f, searchTerm);
         }
         alreadyChecked = true;
      }
      // If there are sub-directories, check those files next.
      foreach (string d in Directory.GetDirectories(sDir)) {
         foreach (string f in Directory.GetFiles(d, docType)) {
            badFile = f.ToString();
            GetToDocPart(f, searchTerm);
         }
         DirSearch(d);
      }
      }
   catch (System.Exception) {
      MessageBox.Show("There was a problem with the file " + badFile);
   }
}

In this code, you first declare a Boolean variable alreadyChecked. This variable is used to ensure that once the root directory has been searched, it is not searched again when the method is called recursively to search any sub-directories.

In the DirSearch method, variables are declared that represent the directory and files within the search directory. Next, the contents of the txtSearchTerm textbox is assigned to the search term String variable.

Then the Directory.GetFiles method is called with the directory path argument, returning the files at that location. Each file is then passed to the GetToDocPart method along with the search term. Next, Directory.GetDirectories is called to determine if there are sub-directories from the current directory. If there are sub-directories, the GetToDocPart method is called again with the files in the sub-directory. However, unlike the previous loop, when control is returned from the GetToDocPart procedure, the DirSearch method is called recursively to continue searching through any additional sub-directories.

If there is a problem opening a file in the GetToDocPart method, control is passed back to the DirSearch method and an error message is displayed listing the path and name of the file with the problem.

Next, add the following procedure to the wordSearchTerm.vb (wordSearchTerm.cs) code window.

Private Sub GetToDocPart(ByVal fileName As String, ByVal searchTerm As String)
   ' Given a file name, retrieve the officeDocument part and search
   ' through the part for the ocuurence of the search term.
   Const documentRelationshipType As String = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
   Const wordmlNamespace As String = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
   Dim myRange As Word.Range = Globals.ThisAddIn.Application.ActiveDocument.Content

   ' If the file is a temp file, ignore it.
   If fileName.IndexOf("~$") > 0 Then
      Return
   End If

   ' Open the package with read/write access.
   Dim myPackage As Package
   myPackage = Package.Open(fileName, FileMode.Open, FileAccess.ReadWrite)
   Using (myPackage)
      Dim relationship As System.IO.Packaging.PackageRelationship
      For Each relationship In myPackage.GetRelationshipsByType(documentRelationshipType)
         Dim documentUri As Uri = PackUriHelper.ResolvePartUri(New Uri("/", UriKind.Relative), relationship.TargetUri)
         Dim documentPart As PackagePart = myPackage.GetPart(documentUri)
         Dim doc As XmlDocument = New XmlDocument()
         doc.Load(documentPart.GetStream())

         ' Manage namespaces to perform Xml XPath queries.
         Dim nt As New NameTable()
         Dim nsManager As New XmlNamespaceManager(nt)
         nsManager.AddNamespace("w", wordmlNamespace)

         ' Specify the XPath expression.
         Dim XPath As String = "//w:document/descendant::w:t"
         Dim nodes As XmlNodeList = doc.SelectNodes(XPath, nsManager)
         Dim result As String = ""
         Dim node As XmlNode

         ' Search each node for the search term.
         For Each node In nodes
            result = node.InnerText + " "
            result = result.IndexOf(searchTerm)
            If result <> -1 Then
               myRange.Text = myRange.Text & vbCrLf & fileName
               Exit For
            End If
         Next
      Next
   End Using

End Sub
private void GetToDocPart(string fileName, string searchTerm)
{
   //  Given a file name, retrieve the officeDocument part.
   const string documentRelationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
   const string wordmlNamespace = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
   Word.Range myRange = Globals.ThisAddIn.Application.ActiveDocument.Content;
   //  If the file is a temp file, ignore it.
   if ((fileName.IndexOf("~$") > 0))
   {
      return;
   }

   //  Open the package with read/write access.
   Package myPackage;
   myPackage = Package.Open(fileName, FileMode.Open, FileAccess.ReadWrite);
   using (myPackage)
   {
      //System.IO.Packaging.PackageRelationship relationship;
      foreach (System.IO.Packaging.PackageRelationship relationship in myPackage.GetRelationshipsByType(documentRelationshipType))
      {
          Uri documentUri = PackUriHelper.ResolvePartUri(new Uri("/", UriKind.Relative), relationship.TargetUri);
          PackagePart documentPart = myPackage.GetPart(documentUri);
          XmlDocument doc = new XmlDocument();
          doc.Load(documentPart.GetStream());
          //  Manage namespaces to perform Xml XPath queries.
          NameTable nt = new NameTable();
          XmlNamespaceManager nsManager = new XmlNamespaceManager(nt);
          nsManager.AddNamespace("w", wordmlNamespace);
          //  Specify the XPath expression.
          string XPath = "//w:document/descendant::w:t";
          XmlNodeList nodes = doc.SelectNodes(XPath, nsManager);
          string result = "";
          //  Search each node for the search term.
          foreach (XmlNode node in nodes)
          {
             result = (node.InnerText + " ");
             int inDex = result.IndexOf(searchTerm);
             if (inDex != -1 )
             {
                myRange.Text = (myRange.Text + ("\r\n" + fileName));
             }
          }
      }
   }
}

After defining namespaces that are needed to open the Open XML Format file package, a Range in the Word 2007 document is specified. This is where the search results will be inserted as the procedure runs. Next, attempting to open temporary documents (back-up documents created when you open a Word 2007 document) will result in an error so the input document is tested. The next code segment opens the Open XML Format package representing the document, with read and write privileges. Then the document-part of the document is retrieved and the content is loaded into an XML document. An XPath query is then run to test for the occurrence of the search term. If the term is found, the path and file name are added to the Range object. And because there is no need to search the document further, the procedure exits the ForEach..Next (foreach) loop.

Testing the Project

In the following steps, you build and test the add-in project.

To test the Add-in project

  1. On the Debug menu, click Start Debugging. The project is built and Word 2007 is displayed.

  2. On the right-side of the Home tab, click the Word Search tab and then click the Display Task Pane button. The Word Search task pane that you created is displayed on the right side of the screen.

  3. In the top textbox, type a directory path that you know contains one or more Word 2007 (*.docx) files.

  4. In the second textbox, type the term you want to search for in those files. The search term is case-sensitive.

  5. Click the Search button to start the search. As the search progresses, for each .docx file found that contains the search term, its directory path and filename are added to the Word document.

  6. Close the document once the search is complete.

Conclusion

By building this project, you have seen the marriage of different technologies into a single useful application. These included the Office Fluent Ribbon, custom task panes, Open XML Format files as well as project development in Microsoft Visual Studio utilizing the Microsoft Visual Studio Tools for the Office System Second Edition. I encourage you to experiment with the project as well as add your own features such allowing the user to specify the type of documents to search in the task pane, allowing both .docx and. docm files to be searched, or adding functionality that allows the user to interrupt the search whenever desired.

Additional Resources

You can find additional information about the technologies discussed in this article at the following locations.