Migrating a Document-Level Customization to an Application-Level Add-In

Summary: Learn how to migrate your document-based solutions to application-level add-ins. Add-ins can customize new features of the 2007 Microsoft Office system, such as application-level task panes and the Ribbon. (43 printed pages)

Brian A. Randell, MCW Technologies, LLC

December 2006

Applies to: 2007 Microsoft Office system, Microsoft Visual Studio 2005 Tools for the 2007 Microsoft Office System, Microsoft Visual Studio 2005 Tools for the Microsoft Office System

Contents

  • Overview: Development Tools for Microsoft Office

  • Building an Office 2003 Solution

  • Design Changes

  • Conclusion

  • Additional Resources

Overview: Development Tools for Microsoft Office

Each release of Microsoft Visual Studio Tools for the Microsoft Office System (Visual Studio Tools for Office) provided developers who program by using Microsoft Visual Basic and Microsoft Visual C# with new ways to extend and enhance the Microsoft Office system. The current release, Microsoft Visual Studio 2005 Tools for the Microsoft Office System, provides several new features, including design-time support for Microsoft Office Excel 2003 and Microsoft Office Word 2003 documents, Document Actions task pane support, rich data binding support, enhanced deployment options, and smart tags support (see What's New in Visual Studio 2005 Tools for Office for more information). But time does not stand still. Currently, the 2007 Microsoft Office system is in late beta release. This new release of Office introduces hundreds of new options for developers. To help managed code developers take advantage of these new features as soon as possible, Microsoft is introducing an updated version of Visual Studio Tools for Office, known as Microsoft Visual Studio 2005 Tools for the 2007 Microsoft Office System (also called Visual Studio 2005 Tools for Office Second Edition). This new release brings additional programmability options to the Visual Studio Tools for Office toolset, before the next full release.

NoteNote

This article was developed and tested with Visual Studio 2005 Tools for Office (the currently released version) and Visual Studio 2005 Tools for Office Second Edition. If you use a later release, your results might be different.

Visual Studio 2005 Tools for Office Second Edition brings long-awaited support for building application-level add-ins, modifying the Ribbon, and creating application-level task panes. Table 1 provides the complete list of supported Visual Studio 2005 Tools for Office Second Edition host applications. It shows that you can build managed application add-ins for the forthcoming Office release and also for some Microsoft Office 2003 applications.

Table 1. Application add-in hosts

Host

Visual Studio 2005 Tools for Office Second Edition

Microsoft Office Access 2003

No

Microsoft Office Excel 2003

Yes

Microsoft Office FrontPage 2003

No

Microsoft Office Outlook 2003

Yes (It is also possible to create Outlook 2003 application add-ins by using Visual Studio 2005 Tools for Office, without using Visual Studio 2005 Tools for Office Second Edition.)

Microsoft Office PowerPoint 2003

Yes

Microsoft Office Project 2003

No

Microsoft Office Publisher 2003

No

Microsoft Office Visio 2003

Yes

Microsoft Office Word 2003

Yes

Microsoft Office Access 2007

No

Microsoft Office Excel 2007

Yes

Microsoft Office InfoPath 2007

Yes

Microsoft Office Outlook 2007

Yes

Microsoft Office PowerPoint 2007

Yes

Microsoft Office Project 2007

No

Microsoft Office Publisher 2007

No

Microsoft Office SharePoint Designer 2007

No

Microsoft Office Visio 2007

Yes

Microsoft Office Word 2007

Yes

As you can see, the future is bright with new and exciting choices. Using Visual Basic or Visual C#, you can now provide richer and possibly even better solutions than you did before. However, these choices are sometimes confusing—especially if you have already created a Visual Studio Tools for Office solution by using an earlier version of the tools.

The Document Actions task pane is a prime example of something made easier for you to access. Microsoft Office XP introduced the task pane as a place to group commonly used commands in a user interface (UI) element that is available while the user is actively working on a document. Compared with dialog boxes, the benefit is obvious—users are not blocked from working while using the task pane as they are while using a modal dialog. In addition, many task panes, such as the Styles and Formatting task pane, enable users to use the task pane and see immediate updates to their documents. Office 2003 introduced smart document programmability that enables you to create your own custom Document Actions task pane. You can attach a Document Actions task pane to a Word 2003 document or to an Excel 2003 workbook. Microsoft exposes the core smart document application programming interface (API) via a COM-based set of interfaces. Now, COM is an old friend. But sometimes, you outgrow your friends. It is okay to get together for dinner, but daily contact is not in the cards. The good news is that the Visual Studio Tools for Office team feels the same way. Their goal is to expose a rich, managed API for you to build great solutions that incorporate the Microsoft Office system without needing to spend much time with COM.

Building an Office 2003 Solution

Visual Studio Tools for Office provides managed support for Document Actions task panes for both Excel 2003 and Word 2003. The best way to understand what works with Document Actions task panes is to review an example. The example I created for this article is a simple solution to create book citations. You provide the solution with an ISBN, and the solution provides all the data needed to cite the book in a Word document. This is accomplished by hooking into Amazon.com's public (and free) XML Web service. You start by building the solution by using Visual Studio Tools for Office. Then, you migrate to the 2007 Office system by using Visual Studio 2005 Tools for Office Second Edition.

The solution presented below is not a one-size-fits-all option. In this example, you first see how to create a document-specific solution by using Word 2003 and a Document Actions task pane. This initial design is simple. Next, you see the solution in Office Word 2007, this time using the new application-level task pane. The Word 2007 solution uses a factored design, something that you can use in your Office 2003 solutions. In some cases, a migration such as the one presented here makes sense. In other cases, a solution that uses a Document Actions task pane might be more appropriate. Regardless, a factored solution designed for reuse is worth doing.

Create the solution by using either Visual Basic or Visual C#. To recreate this sample on your own, you need to get a free Amazon.com Web services developer key. You can find instructions and more information at the Amazon Web Services page.

NoteNote

Although it is possible to write Visual Basic and Visual C# in a very similar fashion, the code in this article generally uses built-in language constructs—such as the Visual Basic MsgBox and WithEvents features—instead of a Microsoft .NET Framework alternative.

To build the Office 2003 solution

  1. On the File menu, point to New, and then click Project.

  2. In the New Project dialog box, in the list of project types, expand either the Visual Basic node or the Visual C# node.

  3. Select the Office node in the list of project types.

  4. In the Templates pane, select Word Document.

  5. Type the name Citations and a location for your customization, and then click OK.

    The Visual Studio Tools for Office Project Wizard opens.

  6. Click OK to accept the default settings.

    After a few moments, the Word 2003 document opens as a tab document inside Microsoft Visual Studio 2005.

  7. On the View menu, click Code to open the code file for the document.

The initial class, ThisDocument, is empty except for two event handlers:ThisDocument_StartupandThisDocument_Shutdown.

The first thing you do is define class-level variables for the Microsoft Windows Forms controls that the Documents Actions task pane will host. Add eight Windows Forms controls: two buttons, one text box, and five labels. The text box is where users type the ISBN of the books they want to look up. One button is for users to submit the ISBN to the Web service. The other button places the citation in the Word document at the current cursor location. Four of the labels display the book's primary author (determined by the Web service), title, publisher, and publication date. The last label is for keeping the user informed of the status of the Web service call while the book information is retrieved.

The following code blocks show all of the modifications to the ThisDocument class.

Public Class ThisDocument
  Private WithEvents btnGetBookInfo As Button
  Private WithEvents btnPutInfoInDoc As Button
  Private lblGetBookInfoStatus As Label

  Private txtISBN As TextBox
  Private lblAuthor As Label
  Private lblPublisher As Label
  Private lblTitle As Label
  Private lblYearPublished As Label

  Private Sub ThisDocument_Startup(ByVal sender As Object, _
    ByVal e As System.EventArgs) Handles Me.Startup

  End Sub

  Private Sub ThisDocument_Shutdown(ByVal sender As Object, _
    ByVal e As System.EventArgs) Handles Me.Shutdown

  End Sub

End Class
namespace CitationsCS
{
  public partial class ThisDocument
  {
    private Button btnGetBookInfo;
    private Button btnPutInfoInDoc;
    private Label lblGetBookInfoStatus;

    private TextBox txtISBN;
    private Label lblAuthor;
    private Label lblPublisher;
    private Label lblTitle;
    private Label lblYearPublished;

    private void ThisDocument_Startup(object sender, System.EventArgs e)
    {
    }

    private void ThisDocument_Shutdown(object sender, System.EventArgs e)
    {
    }

    #region VSTO Designer generated code

    /// <summary>
    /// Required method for Designer support - do not modify
    /// the contents of this method with the code editor.
    /// </summary>
    private void InternalStartup()
    {
      this.Startup += new System.EventHandler(ThisDocument_Startup);
      this.Shutdown += new System.EventHandler(ThisDocument_Shutdown);
    }

    #endregion
  }
}

In the ThisDocument_Startup event handler, the controls are initialized as necessary. Then the code uses the Add method of the ActionsPane object's Controls collection to add each control to the Document Actions task pane in the order in which they are displayed, from top to bottom.

Public Class ThisDocument
  Private Sub ThisDocument_Startup(ByVal sender As Object, _ 
    ByVal e As System.EventArgs) Handles Me.Startup

    btnGetBookInfo = New Button()
    btnGetBookInfo.Text = "Get Book Data"

    btnPutInfoInDoc = New Button()
    btnPutInfoInDoc.Text = "Put Book Data"
    btnPutInfoInDoc.Enabled = False

    lblGetBookInfoStatus = New Label
    lblGetBookInfoStatus.Text = "Ready"
    txtISBN = New TextBox
    txtISBN.Text = String.Empty
    lblAuthor = New Label
    lblYearPublished = New Label
    lblPublisher = New Label
    lblTitle = New Label

    With ActionsPane.Controls
      .Add(btnGetBookInfo)
      .Add(btnPutInfoInDoc)
      .Add(lblGetBookInfoStatus)
      .Add(txtISBN)
      .Add(lblAuthor)
      .Add(lblYearPublished)
      .Add(lblPublisher)
      .Add(lblTitle)
    End With
  End Sub

  Private Sub ThisDocument_Shutdown(ByVal sender As Object, _
    ByVal e As System.EventArgs) Handles Me.Shutdown

  End Sub

End Class
public partial class ThisDocument
{
  private void ThisDocument_Startup(object sender, System.EventArgs e)
  {
    btnGetBookInfo = new Button();
    btnGetBookInfo.Text = "Get Book Data";
    btnGetBookInfo.Click += new EventHandler(btnGetBookInfo_Click);

    btnPutInfoInDoc = new Button();
    btnPutInfoInDoc.Text = "Put Book Data";
    btnPutInfoInDoc.Enabled = false;
    btnPutInfoInDoc.Click += new EventHandler(btnPutInfoInDoc_Click);

    lblGetBookInfoStatus = new Label();
    lblGetBookInfoStatus.Text = "Ready";
    txtISBN = new TextBox();
    txtISBN.Text = string.Empty;
    lblAuthor = new Label();
    lblPublisher = new Label();
    lblTitle = new Label();
    lblYearPublished = new Label();

    ActionsPane.Controls.Add(btnGetBookInfo);
    ActionsPane.Controls.Add(btnPutInfoInDoc);
    ActionsPane.Controls.Add(lblGetBookInfoStatus);
    ActionsPane.Controls.Add(txtISBN);
    ActionsPane.Controls.Add(lblAuthor);
    ActionsPane.Controls.Add(lblPublisher);
    ActionsPane.Controls.Add(lblYearPublished);
    ActionsPane.Controls.Add(lblTitle);
  }
  void btnPutInfoInDoc_Click(object sender, EventArgs e)
  {
    throw new Exception("The method or operation is not implemented.");
  }

  void btnGetBookInfo_Click(object sender, EventArgs e)
  {
    throw new Exception("The method or operation is not implemented.");
  }

  private void ThisDocument_Shutdown(object sender, System.EventArgs e)
  {
  }
}

At this point, there is enough code for you to try things out. Press F5 to start Word and run the solution. The Document Actions task pane opens and all the controls are arranged as in Figure 1.

Figure 1. The completed Document Actions task pane

The completed Document Actions task pane

Now that the basic UI is defined, add logic to get the book information from the Amazon.com Web service. To keep the user informed of the progress of the book data retrieval, a label control displays status information. This next block of code creates a generic helper procedure to change the label's Text property and refresh it.

Public Class ThisDocument
  Private Sub UpdateStatus(ByVal Message As String)
    lblGetBookInfoStatus.Text = Message
    lblGetBookInfoStatus.Update()
  End Sub
End Class
public partial class ThisDocument
{
  private void UpdateStatus(string Message)
  {
    lblGetBookInfoStatus.Text = Message;
    lblGetBookInfoStatus.Update();
  }
}

To use the Amazon.com Web service, add a Web Reference to the project.

To add a Web service reference

  1. In Solution Explorer, right-click the project node, and then click Add Web Reference.

  2. In the Add Web Reference dialog box, type the following address in the URL box, and then click Go:

    http://webservices.amazon.com/AWSECommerceService/2006-05-17/AWSECommerceService.wsdl

    NoteNote

    At the time this article was written, the most current version of the Amazon.com e-commerce Web service WSDL definition had an issue that prevented its use via the generated Web service proxy, so this is the latest version that I could use. The most current version of their WSDL is found at http://webservices.amazon.com/AWSECommerceService/AWSECommerceService.wsdl.

  3. After the Web service description appears, type AmazonECS4 in the Web Reference Name box, and then click Add Reference to complete the process and generate the local proxy class.

Now that the Web reference is ready, add the following three additional fields to theThisDocument class.

Private Const AWS_Access_KeyId As String = "Your Access Key ID"
Private aws As AmazonECS4.AWSECommerceService = Nothing
Private savedISBN As String
private const string AWS_Access_KeyId = "Your Access Key ID";
private AmazonECS4.AWSECommerceService aws = null;
private string savedISBN;

The first field is a constant that represents your Amazon Web service developer access key ID. As mentioned earlier, you can get a free key at the Amazon Web Services page. The second field is a reference to the local Amazon e-commerce proxy that you generated earlier, and the third field is used to store the requested ISBN.

The last bit of code has two parts. One is the code that calls the Amazon Web service, and the other is the code that is used to add the book information to the current document.

The first section of code is the btnGetBookInfo button Click event handler. The first thing the code does is some basic validation of the input data. It checks to ensure that the field is not null or an empty string. You can add additional code to check whether the value that the user enters is a valid ISBN. Next, the code creates an instance of the top-level Amazon Web service class and stores a reference to it in the aws class-level variable. The code initializes the ItemLookupRequest with the necessary data to look up the book information. This data includes the ISBN that the user entered, the type of lookup the Web service should perform, and how much data the Web service should return.

Amazon.com designed the Web service API to be flexible. You can specify different types of information that you want returned in the form of ResponseGroups. For this add-in, the Medium ResponseGroup has all the data that is necessary to populate the task pane's controls. After the code has set up the ItemLookupRequest, it packs the instance into an ItemLookup object. Each ItemLookupRequest can hold up to ten ItemLookupRequest objects. This example uses only one. The ItemLookupRequest is where you provide your Amazon Web service key (which is already defined as a class-level constant).

You have now defined everything the code needs to make the actual Web service call. The aws instance's ItemLookup method executes the Web service call and passes the data to Amazon.com. The method returns an ItemLookupResponse object. Because the method can return results for multiple items, the next bit of code unpacks the first Items collection and then gets the first Item in the collection. In this example, there should be only one Items collection, which contains one Item, because the code is searching by ISBN, which is unique to a single title. The code then accesses the ItemAttributes collection to load the author, publication date, publisher, and title into their respective controls. Finally, yet importantly, the code enables the btnPutInfoInDoc button.

Because things can go wrong, wrap the core code in a Try..Catch..Finally block, as shown. In addition, the code makes calls to the UpdateStatus method to keep the user informed of the lookup progress.

Private Sub btnGetBookInfo_Click(ByVal sender As Object, _
  ByVal e As System.EventArgs) Handles btnGetBookInfo.Click

  Dim finalStatus As String = "Last Lookup Complete"
  btnPutInfoInDoc.Enabled = False

  If txtISBN.Text Is Nothing OrElse txtISBN.Text = String.Empty Then
    MsgBox("Please enter a valid ISBN.", _
      MsgBoxStyle.Exclamation, "Warning")
    txtISBN.Focus()
    Return
  End If

  Try
    UpdateStatus("Preparing ...")

    If aws Is Nothing Then
      aws = New AmazonECS4.AWSECommerceService()
    End If

    ' Define a specific item to look up.
    Dim ilr As New AmazonECS4.ItemLookupRequest
    ilr.ItemId = New String() {txtISBN.Text.Trim()}
    ilr.IdType = AmazonECS4.ItemLookupRequestIdType.ASIN
    ilr.IdTypeSpecified = True
    ilr.ResponseGroup = New String() {"Medium"}

    Dim il As New AmazonECS4.ItemLookup()
    With il
      .AWSAccessKeyId = AWS_Access_KeyId
      .Request = New AmazonECS4.ItemLookupRequest() {ilr}
    End With

    UpdateStatus("Making Request ...")

    ' Make the request from the Web service.
    Dim ilresp As AmazonECS4.ItemLookupResponse = aws.ItemLookup(il)
    UpdateStatus("Processing Response ...")

    ' Unpack the results.
    Dim resultsItems As AmazonECS4.Items = ilresp.Items(0)
    Dim mainItem As AmazonECS4.Item = resultsItems.Item(0)

    ' Load the results into the UI.
    With mainItem.ItemAttributes
      savedISBN = .ISBN
      lblAuthor.Text = .Author(0)
      lblYearPublished.Text = CDate(.PublicationDate).Year.ToString()
      lblPublisher.Text = .Publisher
      lblTitle.Text = .Title
    End With

    btnPutInfoInDoc.Enabled = True

  Catch ex As Exception
    MsgBox(ex.Message, MsgBoxStyle.Critical, ex.Source)
    finalStatus = "Error on last request. Try again."

  Finally
    UpdateStatus(finalStatus)
  End Try
End Sub
void btnGetBookInfo_Click(object sender, EventArgs e)
{
  string finalStatus = "Last Lookup Complete"; 
  btnPutInfoInDoc.Enabled = false; 
  
  if (txtISBN.Text == null || txtISBN.Text == string.Empty) 
  { 
    MessageBox.Show("Please enter a valid ISBN.", "Warning", 
      MessageBoxButtons.OK, MessageBoxIcon.Warning );
    txtISBN.Focus(); 
    return; 
  }

  try 
  { 
    UpdateStatus("Preparing ..."); 
    if (aws == null) 
    { 
      aws = new AmazonECS4.AWSECommerceService(); 
    } 

    // Define a specific item to look up.
    AmazonECS4.ItemLookupRequest ilr = 
      new AmazonECS4.ItemLookupRequest(); 
    ilr.ItemId = new string[] {txtISBN.Text.Trim()}; 
    ilr.IdType = AmazonECS4.ItemLookupRequestIdType.ASIN; 
    ilr.IdTypeSpecified = true; 
    ilr.ResponseGroup = new string[] {"Medium"}; 

    AmazonECS4.ItemLookup il = new AmazonECS4.ItemLookup(); 
    il.AWSAccessKeyId = AWS_Access_KeyId; 
    il.Request = new AmazonECS4.ItemLookupRequest[]{ilr}; 

    UpdateStatus("Making Request ...");

    // Make the request from the Web service.
    AmazonECS4.ItemLookupResponse ilresp = aws.ItemLookup(il); 
    UpdateStatus("Processing Response ..."); 

    // Unpack the results.
    AmazonECS4.Items resultsItems = ilresp.Items[0]; 
    AmazonECS4.Item mainItem = resultsItems.Item[0];

    // Load the results into the UI.
    savedISBN = mainItem.ItemAttributes.ISBN; 
    lblAuthor.Text = mainItem.ItemAttributes.Author[0]; 
    lblYearPublished.Text = System.Convert.ToDateTime( 
      mainItem.ItemAttributes.PublicationDate).Year.ToString(); 
    lblPublisher.Text = mainItem.ItemAttributes.Publisher; 
    lblTitle.Text = mainItem.ItemAttributes.Title; 

    btnPutInfoInDoc.Enabled = true; 
  } 
  catch (Exception ex) 
  { 
    MessageBox.Show(ex.Message, ex.Source, 
      MessageBoxButtons.OK, MessageBoxIcon.Stop);
    finalStatus = "Error on last request. Try again."; 
  } 
  finally 
  { 
    UpdateStatus(finalStatus); 
  } 
}

After the Web service call finishes and the data is loaded into the controls, the code enables the btnPutInfoInDocbutton. The last block of code that you need to add is the Click event handler code. The code creates a string array to hold all of the control data, and then updates the citation string by using String.Format with the values from the array. The code then uses the Range object's InsertAfter method to insert the citation at the current cursor location. After you add this code, the solution is complete and you can test it.

Private Sub btnPutInfoInDoc_Click(ByVal sender As Object, _
  ByVal e As System.EventArgs) Handles btnPutInfoInDoc.Click

  Dim data() As String = {lblTitle.Text, lblAuthor.Text, _
    lblYearPublished.Text, lblPublisher.Text, _
    savedISBN}
  Dim citation As String = _
    String.Format("{0} by {1}. Copyright {2} {3}, {4}", data)
  Me.Range.InsertAfter(citation)
End Sub
void btnPutInfoInDoc_Click(object sender, EventArgs e)
{
  string[] data = { lblTitle.Text, lblAuthor.Text, 
    lblYearPublished.Text, lblPublisher.Text, savedISBN };
  string citation = string.Format(
    "{0} by {1}. Copyright {2} {3}, {4}", data);
  object missingValue = System.Reflection.Missing.Value;
  this.Range(ref missingValue, ref missingValue).InsertAfter(citation);
}

Moving Forward

Although the solution works as presented, it is far from perfect. From a design perspective, storing all of the code in the ThisDocument class prevents reuse. In addition, the control placement in the task pane is functional but plain. More troublesome is that any time you want to use the Amazon Web service task pane, you need to use this particular document. Changing it to be a Word template project would help to some degree. But what is really wanted is an application-level task pane. Thankfully, Word 2007 supports application-level task panes. The next section shows you how to build the solution to run as an application-level task pane. In addition, you can refactor the design so that you can more easily use it in another host application.

Design Changes

The solution is fairly simple, but it can be refactored several ways. The functionality presented can be broken down into three main areas: task pane integration, host document integration, and Web service code. The first step is to create a separate assembly and build a Windows Forms user control to host the UI controls. The next step is to build an assembly that contains all of the Web service code. The last step brings these items together: The user control is connected to the Web service assembly, and then the user control assembly is loaded in an application-level task pane by a Word 2007 application add-in.

Building the User Control

To build the user control, you need a new Windows Control library project. In this version of the solution, you create one large Visual Studio solution with all of the projects defined within it. Another option is to create a separate Visual Studio solution for each item.

To build a user control

  1. On the File menu, point to New, and then click Project.

  2. In the New Project dialog box, in the list of project types, expand either the Visual Basic node or the Visual C# node.

  3. Select the Windows node in the list of project types.

  4. In the Templates pane, select Windows Control Library.

  5. Name the project CitationsUC, specify a location for the user control, name the solution CitationsSolution, select Create directory for solution, and then click OK.

  6. In Solution Explorer, right-click UserControl.vb or UserControl.cs, and then click Rename.

  7. Change the name to Citation.vb or Citation.cs.

Earlier, you added the Windows Forms controls to the task pane programmatically. However, you can build a user control either visually, by using the designer, or in code. The choice is yours. Just remember, you need to add five labels, one text box, and two buttons. How you lay them out is up to you. Figure 2 provides you with an example layout.

Figure 2. The completed Citation user control

The completed Citation user control

After you add controls and lay them out, you need to build the Web service assembly before you write any code in the user control.

Wrap the Web Service Code

After you build the user control, the next step is to create an assembly to host all of the Web service code.

To create the Web service assembly

  1. Open the solution you created earlier.

  2. On the File menu, point to Add, and then click New Project.

  3. In the Add New Project dialog box, in the list of project types, expand either the Visual Basic node or the Visual C# node.

  4. Select the Windows node in the list of project types.

  5. In the Templates pane, select Class Library.

  6. Type the name CitationWS, and then click OK.

  7. In Solution Explorer, right-click Class1.vb or Class1.cs, and then click Rename.

  8. Change the name to Amazon.vb or Amazon.cs.

  9. In Solution Explorer, right-click the CitationWS project node, and then click Add Web Reference.

  10. In the Add Web Reference dialog box, type the following address in the URL field, and then click Go: http://webservices.amazon.com/AWSECommerceService/2006-05-17/AWSECommerceService.wsdl

  11. After the Web service description appears, in the Web Reference Name box, type AmazonECS4, and then click Add Reference to complete the process and generate the local proxy class.

Now that the class library project is set up, replace the empty Amazon class file with the code listed below. The core code is the same as the code used in the btnGetBookInfo button's Click event handler from the Word 2003 solution. However, there are changes:

  • The class has a new Status event defined. The code raises this event to provide the UI with an opportunity to provide the user with progress information, if desired. In the previous example, the code updated a label control directly.

  • Because the code cannot directly update the controls, it needs a way to pass the results data to the caller. You can accomplish this in different ways. In this example, the code uses a nested class called BookData. The BookData class is the return value for a new method, GetBookInfo, which accepts an ISBN as a string as input. In this method, the code creates a local instance of BookData, stores the ISBN in the instance, and then sets up the Web service call just like in the Word 2003 example. When the Web service call finishes, the code stores the return data in the fields of the BookData instance rather than updating the UI directly, because the code does not have access to the UI.

  • The code throws exceptions where, in the Word 2003 solution, calls to display a message box were used.

  • Instead of trapping exceptions, the code allows the runtime to re-throw exceptions to the caller.

Public Class Amazon
  Public Event Status(ByVal Message As String)

  Private Const AWS_Access_KeyId As String = "Your Access Key ID"
  Private aws As AmazonECS4.AWSECommerceService = Nothing

  ' Nested class used to return search results.
  Public Class BookData
    Public ISBN As String
    Public Author As String
    Public Publisher As String
    Public Title As String
    Public YearPublished As String
  End Class

  Public Function GetBookInfo(ByVal ISBN As String) As BookData
    If ISBN Is Nothing OrElse ISBN = String.Empty Then
      Throw New Exception("Please provide a valid ISBN.")
    Else
      RaiseEvent Status("Preparing ...")

      If aws Is Nothing Then
        aws = New AmazonECS4.AWSECommerceService()
      End If

      ' Create an instance of the return value.
      Dim localBookData As New BookData
      localBookData.ISBN = ISBN.Trim()

      ' Define a specific item to look up.
      Dim ilr As New AmazonECS4.ItemLookupRequest
      ilr.ItemId = New String() {localBookData.ISBN}
      ilr.IdType = AmazonECS4.ItemLookupRequestIdType.ASIN
      ilr.IdTypeSpecified = True
      ilr.ResponseGroup = New String() {"Medium"}

      Dim il As New AmazonECS4.ItemLookup()
      With il
        .AWSAccessKeyId = AWS_Access_KeyId
        .Request = New AmazonECS4.ItemLookupRequest() {ilr}
      End With

      RaiseEvent Status("Making Request ...")
      ' Make the request from the Web service.
      Dim ilresp As AmazonECS4.ItemLookupResponse = aws.ItemLookup(il)
      RaiseEvent Status("Processing Response ...")

      Dim resultsItems As AmazonECS4.Items = ilresp.Items(0)
      Dim mainItem As AmazonECS4.Item = resultsItems.Item(0)
      With mainItem.ItemAttributes
        localBookData.Author = .Author(0)
        localBookData.YearPublished = _
        CDate(.PublicationDate).Year.ToString()
        localBookData.Publisher = .Publisher
        localBookData.Title = .Title
      End With

      RaiseEvent Status("Last Lookup Complete")

      Return localBookData
    End If
  End Function
End Class
public class Amazon
{
  public delegate void StatusEventHandler(string Message);
  public event StatusEventHandler Status;

  private const string AWS_Access_KeyId = "Your Access Key ID";
  private AmazonECS4.AWSECommerceService aws = null;

  // Nested class used to return search results.
  public class BookData
  {
    public string ISBN;
    public string Author;
    public string Publisher;
    public string Title;
    public string YearPublished;
  }

  public BookData GetBookInfo(string ISBN)
  {
    if (ISBN == null || ISBN == string.Empty)
    {
      throw new Exception("Please provide a valid ISBN.");
    }
    else
    {
      if (Status != null)
      {
        Status("Preparing ...");
      }
      if (aws == null)
      {
        aws = new AmazonECS4.AWSECommerceService();
      }

      // Create an instance of the return value.
      BookData localBookData = new BookData();
      localBookData.ISBN = ISBN.Trim();

      // Define a specific item to look up.
      AmazonECS4.ItemLookupRequest ilr = 
        new AmazonECS4.ItemLookupRequest();
      ilr.ItemId = new string[] { localBookData.ISBN };
      ilr.IdType = AmazonECS4.ItemLookupRequestIdType.ASIN;
      ilr.IdTypeSpecified = true;
      ilr.ResponseGroup = new string[] { "Medium" };

      AmazonECS4.ItemLookup il = new AmazonECS4.ItemLookup();
      il.AWSAccessKeyId = AWS_Access_KeyId;
      il.Request = new AmazonECS4.ItemLookupRequest[] { ilr };

      // Make the request from the Web service.
      if (Status != null)
      {
        Status("Making Request ...");
      }
      AmazonECS4.ItemLookupResponse ilresp = aws.ItemLookup(il);
      if (Status != null)
      {
        Status("Processing Response ...");
      }

      AmazonECS4.Items resultsItems = ilresp.Items[0];
      AmazonECS4.Item mainItem = resultsItems.Item[0];
      localBookData.Author = mainItem.ItemAttributes.Author[0];
      localBookData.YearPublished = 
        System.Convert.ToDateTime(
        mainItem.ItemAttributes.PublicationDate).Year.ToString();
      localBookData.Publisher = mainItem.ItemAttributes.Publisher;
      localBookData.Title = mainItem.ItemAttributes.Title;
      if (Status != null)
      {
        Status("Last Lookup Complete");
      }
      return localBookData;
    }
  }
}

This new design enables you to reuse the Web service without regard to the type of hosting application. If you want, now would be a good time to compile everything. If there are no errors, move on to the next section.

Wire Things Up

As mentioned earlier, Visual Studio 2005 Tools for Office Second Edition provides managed application add-ins as a new feature. Use a Word 2007 add-in project to bring things together.

To wire up the projects

  1. On the File menu, point to Add, and then click New Project.

  2. In the Add New Project dialog box, in the list of project types, expand either the Visual Basic node or the Visual C# node.

  3. Expand the Office node, and then select the 2007 Add-ins node.

  4. In the Templates pane, select Word Add-in.

  5. Type the name CitationsAddinWord, and then click OK.

  6. In Solution Explorer, right-click the CitationsAddinWord project node, and then click Add Reference.

  7. Click the Projects tab, select the CitationsUC project, and then click OK.

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

  9. Click the Projects tab, select the CitationsWS project, and then click OK.

  10. In Solution Explorer, right-click the CitationsAddinWord project node, and then click Set as StartUp Project.

The 2007 Office system applications that support application-level task panes support more than one application-level task pane being active at the same time, so the code to create one is slightly different from the code for creating Document Actions task panes. The following code shows how to add the user control, created earlier, to an application-level task pane.

Imports Microsoft.Office.Tools

Public Class ThisAddIn
  Private WithEvents citationsUI As CitationsUC.Citation
  Private citationTaskPane As CustomTaskPane

  Private Sub ThisAddIn_Startup(ByVal sender As Object, _
    ByVal e As System.EventArgs) Handles Me.Startup

    citationsUI = New CitationsUC.Citation
    citationTaskPane = CustomTaskPanes.Add( _
      citationsUI, "Citations")

    citationTaskPane.Visible = True
  End Sub

  Private Sub ThisAddIn_Shutdown(ByVal sender As Object, _
    ByVal e As System.EventArgs) Handles Me.Shutdown

    CustomTaskPanes.Remove(citationTaskPane)
  End Sub
End Class
using Microsoft.Office.Tools;

namespace CitationsAddinWordCS
{
  public partial class ThisAddIn
  {
    private CitationsUC.Citation citationsUI;
    private CustomTaskPane citationTaskPane;

    private void ThisAddIn_Startup(object sender, System.EventArgs e)
    {
      citationsUI = new CitationsUC.Citation();
      citationTaskPane = CustomTaskPanes.Add(citationsUI, "Citations");

      citationTaskPane.Visible = true;
    }

    private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
    {
      CustomTaskPanes.Remove(citationTaskPane);
    }
  }
}

A few things are worth noting:

  • There is a collection of custom task panes. It is possible for a host application to display more than one task pane at a time.

  • You must make the task pane visible. However, this is something that you should do only during testing. In the 2007 Office system, UI elements such as task panes should never just appear. The best practice is to add a UI element to the Ribbon to enable the user to make the Citations task pane visible.

  • Because the custom task pane is no longer associated with a particular document, it needs to be removed when the add-in is unloaded. Naturally, if the host is unloaded this is not a big deal. However, the user could unload the add-in at any time, and your UI elements should not remain behind.

You have written the code to load the user control. Now, you can write the code to load the book data into the user control from the Web service. After you do that, the next bit of code you need to write is the code to put book data into the active document. After that, you work on hooking your add-in into the Ribbon to give it the final touch of goodness.

Hooking up the user control to the Web service requires only a small amount of code. You already set the reference to the Web service assembly. You need to define two class-level variables: one for the Web service, and one for the ISBN that the user entered. Also, you need to define two event handlers: one for the btnGetBookInfoobject's Click event and one for the Amazon class's Status event. The following listing shows the Citation class.

Public Class Citation
  Private WithEvents amznWS As CitationsWS.Amazon
  Private savedISBN As String

  Private Sub btnGetBookInfo_Click(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles btnGetBookInfo.Click

    If amznWS Is Nothing Then
      amznWS = New CitationsWS.Amazon
    End If

    Dim bookInfo As CitationsWS.Amazon.BookData = Nothing
    Try
      bookInfo = amznWS.GetBookInfo(txtISBN.Text)

    Catch ex As Exception
      MsgBox(ex.Message, MsgBoxStyle.Critical, ex.Source)
      lblGetBookInfoStatus.Text = "Error on last request. Try again"
      Return
    End Try
    
    If bookInfo IsNot Nothing Then
      With bookInfo
        savedISBN = .ISBN
        lblAuthor.Text = .Author
        lblPublisher.Text = .Publisher
        lblTitle.Text = .Title
        lblYearPublished.Text = .YearPublished
      End With
      lblGetBookInfoStatus.Text = "Ready"
    End If
  End Sub

  Private Sub amznWS_Status(ByVal Message As String) Handles amznWS.Status
    lblGetBookInfoStatus.Text = Message
    lblGetBookInfoStatus.Refresh()
  End Sub
End Class
public partial class Citation : UserControl
{
  
  private CitationsWS.Amazon amznWS;
  private string savedISBN;

  public Citation()
  {
    InitializeComponent();
  }

  private void btnGetBookInfo_Click(object sender, EventArgs e)
  {
    if ((amznWS == null))
    {
      amznWS = new CitationsWS.Amazon();
      amznWS.Status += 
        new CitationsWS.Amazon.StatusEventHandler(amznWS_Status);
    }

    CitationsWS.Amazon.BookData bookInfo = null;
    try
    {
      bookInfo = amznWS.GetBookInfo(txtISBN.Text);
    }
    catch (Exception ex)
    {
      MessageBox.Show(ex.Message, ex.Source, 
        MessageBoxButtons.OK, MessageBoxIcon.Stop);
      lblGetBookInfoStatus.Text = "Error on last request. Try again";
      return;
    }
    
    if (bookInfo != null)
    {
      savedISBN = bookInfo.ISBN;
      lblAuthor.Text = bookInfo.Author;
      lblPublisher.Text = bookInfo.Publisher;
      lblTitle.Text = bookInfo.Title;
      lblYearPublished.Text = bookInfo.YearPublished;
      lblGetBookInfoStatus.Text = "Ready";
    }
  }

  void amznWS_Status(string Message)
  {
    lblGetBookInfoStatus.Text = Message;
    lblGetBookInfoStatus.Refresh();
  }
}

It takes a bit more work to get the citation information into Word 2007 than it does to get the information into Word 2003. In fact, because you have factored the design, the user control should not have a dependency on running inside Word, or any other Office application. One way to accomplish this is to expose the data from the labels as properties of the control. In addition, you need to define an event so that any host using the user control can know when a user clicks the btnPutInfoInDoc button.

NoteNote

You could expose the button by changing its visibility from Friend or internal to Public. However, that would expose the entire control and break encapsulation.

First, wrap the Text property of all the relevant label controls, so that the data is accessible from the host application. Next, define an event that signals the host add-in that the user wants the data transferred from the user control to the active document. Finally, write code that raises the event in thebtnPutInfoInDocbutton's Click event handler. The following code shows these modifications in the Citation class.

Public Class Citation
  Private WithEvents amznWS As CitationsWS.Amazon
  Private savedISBN As String
  Public Event PutInfo As System.EventHandler

  Private Sub btnPutInfoInDoc_Click(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles btnPutInfoInDoc.Click

    RaiseEvent PutInfo(Me, EventArgs.Empty)
  End Sub

  Public ReadOnly Property Author() As String
    Get
      Return lblAuthor.Text
    End Get
  End Property
  Public ReadOnly Property ISBN() As String
    Get
      Return savedISBN
    End Get
  End Property
  Public ReadOnly Property Publisher() As String
    Get
      Return lblPublisher.Text
    End Get
  End Property
  Public ReadOnly Property Title() As String
    Get
      Return lblTitle.Text
    End Get
  End Property
  Public ReadOnly Property YearPublished() As String
    Get
      Return lblYearPublished.Text
    End Get
  End Property
End Class
public partial class Citation : UserControl
{
  private CitationsWSCS.Amazon amznWS;
  private string savedISBN;
  public event System.EventHandler PutInfo;

  private void btnPutInfoInDoc_Click(object sender, EventArgs e)
  {
    PutInfo(this, EventArgs.Empty);
  }

  public string Author
  {
    get { return lblAuthor.Text; }
  }
  public string ISBN
  {
    get { return savedISBN; }
  }
  public string Publisher
  {
    get { return lblPublisher.Text; }
  }
  public string Title
  {
    get { return lblTitle.Text; }
  }
  public string YearPublished
  {
    get { return lblYearPublished.Text; }
  }
}

Next, add an event handler to the ThisAddIn class that catches the PutInfo event and loads the citation into the active Word document. In the event handler, pull the data from the user control and put it into a string array. Then, pass the array to the String.Format method, which creates the citation string. Then, insert the citation into the active document, just like in the Word 2003 solution.

Public Class ThisAddIn
  Private Sub citationsUI_PutInfo(ByVal sender As Object, _
    ByVal e As System.EventArgs) Handles citationsUI.PutInfo

    Dim data(4) As String
    With citationsUI
      data(0) = .Title
      data(1) = .Author
      data(2) = .YearPublished
      data(3) = .Publisher
      data(4) = .ISBN
    End With
    
    Dim citation As String = _
      String.Format("{0} by {1}. Copyright {2} {3}, {4}", Data)

    Me.Application.ActiveDocument.Range.InsertAfter(citation)
  End Sub
End Class
public partial class ThisAddIn
{
  private void ThisAddIn_Startup(object sender, System.EventArgs e)
  {
    citationsUI = new CitationsUCCS.Citation();
    citationTaskPane = CustomTaskPanes.Add(citationsUI, "Citations");
    citationsUI.PutInfo += new EventHandler(citationsUI_PutInfo);
    citationTaskPane.Visible = true;
  }

  void citationsUI_PutInfo(object sender, EventArgs e)
  {
    string[] data = new string[5];
    data[0] = citationsUI.Title;
    data[1] = citationsUI.Author;
    data[2] = citationsUI.YearPublished;
    data[3] = citationsUI.Publisher;
    data[4] = citationsUI.ISBN;
    string citation = 
      string.Format("{0} by {1}. Copyright {2} {3}, {4}", data);
          object missingValue = System.Reflection.Missing.Value;
    this.Application.ActiveDocument.Range(
      ref missingValue, ref missingValue).InsertAfter(citation);
  }
}

Start the solution and see whether everything works. The result is the same as before; however, now you have a much more factored and professional design, which makes it much easier to reuse the components in other Office host applications or even in other managed applications. However, you are not finished yet.

Adding Ribbon Support

Currently, the add-in makes the Citations task pane visible at startup. This works, until the user closes the task pane. How does the user get the task pane back? That type of behavior also goes against the design standards provided by the Office team. You need to add a button of some type to the Ribbon so that the user can control when the task pane is visible.

To add Ribbon support, follow these basic steps:

  1. Add a toggle button to an existing Ribbon tab (the Insert tab seems logical).

  2. Add a callback that is called when the user clicks the button.

  3. Change the visible state of the task pane when the callback occurs.

  4. Ensure that the button shows the correct state of the task pane if the user closes it from the task pane rather than from the button.

Visual Studio 2005 Tools for Office Second Edition provides support for programming the Ribbon.

To add Ribbon support to your project

  1. In Solution Explorer, select the Word add-in project.

  2. On the Project menu, click Add New Item.

  3. In the Add New Item dialog box, select Ribbon support.

  4. Change the file name to RibbonCode.vb or RibbonCode.cs, and then click Add.

Visual Studio adds two new items to the project: a RibbonCode source file and RibbonCode.xml, which is set to compile as an embedded resource. You need to update the XML file so that the new toggle button appears correctly on the Insert tab. To do this, open RibbonCode.xml and replace the default XML with the following XML.

<customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui" 
          onLoad="OnLoad">
  <ribbon>
    <tabs>
      <tab idMso="TabInsert">
        <group id="amznCitation"
               label="Citation">
          <toggleButton id="showCitationActionPane" 
                        size="large"
                        label="Citation from WS"
                        screentip="Insert a citation from Amazon"
                        onAction="showCitationActionPane_onAction" 
                        imageMso="CitationInsert" />
        </group>
      </tab>
    </tabs>
  </ribbon>
</customUI>

The XML tells the Ribbon to add a new group to the Insert tab. In addition, the XML declares that the button should be a large toggle button, have a label and ScreenTip, use a built-in image, and call a method named showCitationActionPane_onAction when the user clicks the button.

Now you can modify the class that contains the Ribbon support code. Uncomment the section of code below the Imports or using statements. This code tells the Office host application that your add-in wants to provide custom handlers for the Ribbon. The code implements this as two classes. The first section of code is a partial class that is linked to your add-in class. The second class, RibbonCode, provides the necessary callback points to provide the XML to the Ribbon when requested, and to process Ribbon events. After you uncomment the code, change the name of the default callback method from OnToggleButton1 to showCitationActionPane_onAction. The method is inside a region that is labeled Ribbon Callbacks. After you do this, you can try out your changes and see if the toggle button shows up correctly on the Insert tab. In addition, if you click the button, you should receive message boxes that tell you the state of the toggle button.

Now that the core infrastructure is in place, you can add the necessary code to change the visibility state of the task pane, based on the status of the toggle button. The problem is that the code that processes the callback is in a different class from the add-in class, which holds the reference to the task pane. To process the callback, the RibbonCode class needs a reference to the add-in class. An easy way for you to accomplish this is to modify the RibbonCode class to have a constructor that accepts a reference to the add-in class. Use the following code as an example.

Public Sub New(ByVal Parent As ThisAddIn)
  parentAddIn = Parent
End Sub
public RibbonCode(ThisAddIn Parent)
{
  parentAddIn = Parent;
}

Add a class-level variable named m_Parent of the type ThisAddin. Next, uncomment and change the RequestService method that creates an instance of the RibbonCode class so that it uses this new constructor. The following code shows the changes you need to make.

Protected Overrides Function RequestService( _
  ByVal serviceGuid As Guid) As Object

  If serviceGuid = GetType(Office.IRibbonExtensibility).GUID Then
    If ribbon Is Nothing Then
      ribbon = New RibbonCode(Me)
    End If
    Return ribbon
  End If

  Return MyBase.RequestService(serviceGuid)
End Function
protected override object RequestService(Guid serviceGuid)
{
  if (serviceGuid == typeof(Office.IRibbonExtensibility).GUID)
  {
    if (ribbon == null)
      ribbon = new RibbonCode(this);
    return ribbon;
  }

  return base.RequestService(serviceGuid);
}

Now it is possible for you to modify the toggle button's OnAction callback method to change the visibility state of the task pane. Replace the existing showCitationActionPane_onAction procedure with the following code.

Public Sub showCitationActionPane_onAction( _
  ByVal control As Office.IRibbonControl, _
  ByVal isPressed As Boolean)

  Me.parentAddIn.citationTaskPane.Visible = isPressed
End Sub
public void showCitationActionPane_onAction(
  Office.IRibbonControl control, bool isPressed)
{
  this.parentAddIn.citationTaskPane.Visible = isPressed;
}

However, for this code to work, you need to change the visibility of the task pane in the ThisAddin class. Open the ThisAddin class file, and change the visibility of the citationTaskPane variable from private to Friend (if you are using Visual Basic), or internal (if you are using Visual C#). You also need to remove the line of code from the ThisAddIn_Startup procedure that makes the task pane visible by default. After you do these two things, try it out. You should be able to click the button to change the visibility of the task pane.

You are almost finished. There is still one issue to work out: If a user shows the task pane and then uses the X in the upper corner of the task pane to close it, the toggle button's state is not changed. To fix this, you need to add code to determine the correct state of the toggle button. This requires three changes. First, add a callback procedure to the RibbonCode class that the Ribbon will use to query the state of the toggle button. Add the following code to the RibbonCode class.

Public Function showCitationActionPane_getPressed( _
  ByVal control As Office.IRibbonControl) As Boolean

  Return Me.parentAddIn.citationTaskPane.Visible
End Function
public bool showCitationActionPane_getPressed(
  Office.IRibbonControl control)
{
  return this.parentAddIn.citationTaskPane.Visible;
}

Second, add the new callback procedure to the Ribbon. To do this, add an extra attribute to the existing XML. Use the following XML as a guide.

<customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui" 
          onLoad="OnLoad">
  <ribbon>
    <tabs>
      <tab idMso="TabInsert">
        <group id="amznCitation"
               label="Citation">
          <toggleButton id="showCitationActionPane" 
                        size="large"
                        label="Citation from WS"
                        screentip="Insert a citation from Amazon"
                        onAction="showCitationActionPane_onAction" 
                        getPressed="showCitationActionPane_getPressed"
                         imageMso="CitationInsert" />
        </group>
      </tab>
    </tabs>
  </ribbon>
</customUI>

Last, add code that tells the Ribbon to check its state. To do this, provide an event handler for the VisibleChanged event of the task pane. In this event handler, the code needs to invalidate the toggle button, which in turn causes the Ribbon to call the previously added callback procedure.

First, add the following event handler to the RibbonCode class.

Public Sub OnVisibleChanged( _
  ByVal sender As Object, ByVal e As EventArgs)

  Me.ribbon.InvalidateControl("showCitationActionPane")
End Sub
public void OnVisibleChanged(object sender, EventArgs e)
{
  this.ribbon.InvalidateControl("showCitationActionPane");
}

The code you just added invalidates the toggle button by passing the ID of the toggle button to the InvalidateControl method. Next, modify the ThisAddIn_Startup procedure to hook this newly added event handler.

Private Sub ThisAddIn_Startup(ByVal sender As Object, _
  ByVal e As System.EventArgs) Handles Me.Startup
  citationsUI = New CitationsUC.Citation
  citationTaskPane = CustomTaskPanes.Add( _
    citationsUI, "Citations")

  AddHandler citationTaskPane.VisibleChanged, _
    AddressOf ribbon.OnVisibleChanged
End Sub
private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
  citationsUI = new CitationsUCCS.Citation();
  citationTaskPane = CustomTaskPanes.Add(citationsUI, "Citations");
  citationsUI.PutInfo += new EventHandler(citationsUI_PutInfo);
  citationTaskPane.VisibleChanged += 
    new EventHandler(ribbon.OnVisibleChanged);
}

Now, when the host loads the Ribbon support code, the class hooks the VisibleChanged event. If the user closes the task pane, the VisibleChanged event is raised, which causes the code to invalidate the toggle button. This then causes the Ribbon to call the showCitationActionPane_getPressed procedure to get its current pressed state. Give it a try and see if it works.

It Works, but It Can Be Better

The add-in works with the Ribbon. However, the solution is not factored like the rest of the add-in is. The Ribbon is linked to a particular host application. This makes it impossible for you to reuse the Ribbon code in another host application without making changes to the implementation. What you need to do is move the Ribbon code and XML into a separate assembly. The Ribbon code needs to be able to communicate with any host that uses the Citations task pane, without knowing what add-in is hosting it. To make this work, define an interface that the add-in can implement in the RibbonCode source file.

' Add to the top of the source file with the other Imports statements.
Imports Microsoft.Office.Tools

Public Interface ICitationParent
  ReadOnly Property CitationTaskPane() As CustomTaskPane
End Interface
//  Add to the top of the source file with the other using statements.
using Microsoft.Office.Tools;

namespace CitationsAddinWordCS
{
  public interface ICitationParent
  {
    CustomTaskPane CitationTaskPane
    { get; }
  }
}

The ThisAddin class needs to implement this interface. In addition, you need to remove the code you added to the ThisAddIn_Startup procedure that hooks the VisibleChanged event. You also need to change the visibility setting of the CustomTaskPane back to private and modify its name. The following code shows the relevant parts of the updated class.

Public Class ThisAddIn
  Implements ICitationParent

  Private WithEvents citationsUI As CitationsUC.Citation
  Private m_citationTaskPane As CustomTaskPane

  Private Sub ThisAddIn_Startup(ByVal sender As Object, _
    ByVal e As System.EventArgs) Handles Me.Startup
    citationsUI = New CitationsUC.Citation
    m_citationTaskPane = CustomTaskPanes.Add( _
      citationsUI, "Citations")
    ' AddHandler call removed.
  End Sub

  Private Sub ThisAddIn_Shutdown(ByVal sender As Object, _
    ByVal e As System.EventArgs) Handles Me.Shutdown

    CustomTaskPanes.Remove(m_citationTaskPane)
  End Sub

  Public ReadOnly Property CitationTaskPane() As _
    CustomTaskPane Implements ICitationParent.CitationTaskPane

    Get
      Return m_citationTaskPane
    End Get
  End Property
End Class
public partial class ThisAddIn : ICitationParent 
{
  private CitationsUCCS.Citation citationsUI;
  private CustomTaskPane m_citationTaskPane;

  public CustomTaskPane CitationTaskPane
  {
    get { return m_citationTaskPane; }
  }

  private void ThisAddIn_Startup(object sender, System.EventArgs e)
  {
    citationsUI = new CitationsUCCS.Citation();
    m_citationTaskPane = CustomTaskPanes.Add(citationsUI, "Citations");
    citationsUI.PutInfo += new EventHandler(citationsUI_PutInfo);
    //  Removed event handler for citationTaskPane.VisibleChanged.
  }

  private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
  {
    CustomTaskPanes.Remove(m_citationTaskPane);
  }
}

You need to make a set of modifications to the RibbonCode class and the RequestService method, in addition to those you just made to the ThisAddin class. The first change to make is to the parentAddIn class-level variable's type, from ThisAddIn to ICitationParent. Then remove the custom constructor that you added earlier.

One issue with the way an Office host loads an add-in is that Office hooks up the Ribbon code before the Startup method completes. This prevents the Ribbon code from adding an event handler during construction—the task pane does not exist yet. Instead, add a property that enables the add-in to set the Ribbon code parent during the Startup method. In addition, because you removed the custom constructor, you need to fix the RequestService method to use the default constructor of the RibbonCode class. The following code shows the new property you need to add to the RibbonCode class. The setter in this property adds the event handler for the task pane's VisibleChanged event.

NoteNote

If you are using C#, you need to fix a capitalization compile error in bothshowCitationActionPane_onAction and showCitationActionPane_getPressed related to the object name CitationTaskPane. It is listed as citationTaskPane and needs to be CitationTaskPane.

Public Property Parent() As ICitationParent
  Get
    Return m_Parent
  End Get
  Set(ByVal value As ICitationParent)
    parentAddIn = value
    AddHandler parentAddIn.CitationTaskPane.VisibleChanged, _
      AddressOf Me.OnVisibleChanged
  End Set
End Property
public ICitationParent Parent
{
  get { return m_Parent; }
  set 
  {
    parentAddIn = value;
    parentAddIn.CitationTaskPane.VisibleChanged += 
      new EventHandler(this.OnVisibleChanged);
  }
}

To finish, add the following line of code as the last line in the ThisAddIn_Startup procedure.

Me.ribbon.Parent = Me
this.ribbon.Parent = this;

Assuming everything compiles, you can now factor out the Ribbon code and XML into their own class library assembly. Reference this new assembly from the Word add-in and move the code.

To factor the Ribbon code and XML

  1. On the File menu, point to Add, and then click New Project.

  2. In the Add New Project dialog box, in the list of project types, expand either the Visual Basic node or the Visual C# node.

  3. Select the Windows node.

  4. In the Templates pane, select Class Library.

  5. Type the name CitationsRibbon, and then click OK.

  6. In Solution Explorer, right-click the CitationsAddinWord project node, and then click Add Reference.

  7. Click the Projects tab, select the CitationsRibbon project, and then click OK.

  8. In Solution Explorer, in the CitationsRibbon project, right-click Class1.vb or Class1.cs, and then click Delete. Click OK to confirm the deletion.

  9. In Solution Explorer, right-click the CitationsRibbon project node, point to Add, and then click Existing Item.

  10. In the Add Existing Item dialog box, navigate to the folder that contains the RibbonCode.vb or RibbonCode.cs file, and the RibbonCode.xml file. Adjust the dialog box to show all files, select both files, and then click Add.

  11. In Solution Explorer, select the RibbonCode.xml file. Then, by using the Properties window, change the Build Action property to Embedded Resource.

  12. In Solution Explorer, right-click the CitationsRibbon project node, and then click Add Reference.

  13. Click the .NET tab, and then select the following assemblies (press CTRL to select multiple assemblies):

    • Microsoft.Office.Tools.Common

    • Microsoft.Office.Tools.Common2007

    • Microsoft.VisualStudio.Tools.Applications.Runtime

  14. Click OK.

  15. In Solution Explorer, right-click the CitationsRibbon project node, and then click Add Reference.

  16. Click the COM tab, select Microsoft Office 12.0 Object Library, and then click OK.

  17. In Solution Explorer, in the CitationsAddInWord project, right-click RibbonCode.xml, and then click Delete. Click OK to confirm the deletion.

In the CitationsRibbon project, open the RibbonCode.vb or RibbonCode.cs source file. Remove the Imports or using statement for System.Windows.Forms. You also need to remove all of the code related to the partial class ThisAddIn. If you are using C#, change the namespace from CitationsAddinWord to CitationsRibbon. Back in the CitationsAddInWord project, open the RibbonCode source file. Remove the RibbonCode class and the ICitationParent interface definition. Then, add the following Imports or using statement to the top of the file in the RibbonCode class file and the ThisAddin class file, both in the CitationsAddInWord project.

Imports CitationsRibbon
using CitationsRibbon;

The last thing you need to do is modify the GetCustomUI method of the RibbonCode class in your CitationsRibbon project to pass the correct fully qualified name for the XML resource in the call to GetResourceText. At this point, you can compile and test. Now that you have completely factored the design, you can modify the add-in for another host without redoing a lot of your work.

Conclusion

The ability to create managed add-ins and application-level task panes really opens the door to a world of new possibilities when it comes to solutions built on top of Microsoft Office applications. With just a bit of effort, you can factor your designs for greater reuse in other solutions.

About the Author

Brian A. Randell is a senior consultant with MCW Technologies, LLC. Brian spends his time teaching Microsoft technologies to developers, working with new and emerging technologies like the 2007 Microsoft Office system, Visual Studio 2005, and Visual Studio Team System. He also helps clients worldwide, such as Microsoft, American Honda, and DELL, with system design and implementation. As a Microsoft MVP, Brian enjoys helping people get the most out of their software. He does this through training, consulting, and speaking at events such as VSLive!, TechEd, and the PDC. In addition, Brian shares through the written word. He is the co-author of Effective Visual Basic and has written articles for MSDN Magazine and Microsoft. He is a member of Pluralsight's technical staff and author of Pluralsight's Applied Team System course. You can reach Brian via his blog.

Additional Resources

To learn more about the products and technologies mentioned or used in this article, see these resources: