Export (0) Print
Expand All

Search Inbox Data Using Smart Tags in Word 2003

Office 2003

This content is no longer actively maintained. It is provided as is, for anyone who may still be using these technologies, with no warranties or claims of accuracy with regard to the most recent product version or service release.

Summary:   Link your data points in Microsoft Office Word 2003 to Inbox data stored in Microsoft Exchange Server. Use smart tags in Word to create search queries executed against the Exchange message store. Search the message store programmatically to acquire results. Then, import search result data into the Word document. (17 printed pages)

John R. Durant, Microsoft Corporation

September 2004

Applies to: Microsoft Office Word 2003, Microsoft Exchange Server 2003, Microsoft Exchange Server 2000

Contents

Whether composing e-mail messages, letters, or other documents, no program can compare to the power of Microsoft Office Word 2003. You have access to powerful templates, styles, Research services, and so much more. Smart tags make the experience even better by allowing you take action based on the text you type in the Word document. For example, when you type a person's name, Word recognizes it as a person's name and allows you to quickly search for the name in your Outlook contacts and take action from there.

This works great for contacts. But, what about other types of content? Using Outlook, you can store tasks, contacts, appointments, notes, e-mails, posts, and so much more in the Exchange message store. A smart tag that marks up words in a document and connects them to these other content types in the message store is of similar value, but no such built-in smart tags exists. Fortunately, you can develop one fairly easily.

This smart tag has a recognizer that evaluates textual input in Word and determines whether a typed word matches a term in a pre-defined list of search terms. You create this term list and store the values in an XML file. The smart tag DLL reads this file when Word first loads the smart tag and caches the terms in memory. Figure 1 shows the contents of the XML file containing the term list.

Figure 1. The list of terms for recognition

Term list in XML file

When Word matches a typed word to one of the terms in the list, it assigns an attribute to the matched word, marking it up as a smart tag. You can see a recognized term in a Word document in Figure 2.

Figure 2. A recognized term marked up with a smart tag

Recognized term marked up with smart tag

You can then activate the smart tag menu by causing the cursor to hover over the marked up word. The smart tag menu displays a custom item for searching the mailbox (Figure 3).

Figure 3. Smart tag actions menu for a recognized term

Smart tag actions menu

Clicking this item causes the smart tag to execute code that searches the Exchange message store for e-mail messages in your inbox whose subject line contains the search term. The smart tag displays the results in a Windows form with a DataGrid that lists mail items whose subject contains the search term (Figure 4).

Figure 4. Search results in a Windows form

Search results displayed in Windows form

In this case, the DataGrid shows only the fields for the e-mail subject and the sender's information. However, behind the scenes other e-mail fields (such as the body text) are retrieved though not displayed. By simply changing the properties of the grid, you can show these hidden fields. The article explains how this is accomplished later on.

The final feature of this example allows you to insert the main text of a selected e-mail message into the Word document. You do this by hovering over a row in the DataGrid and right-clicking the item. The code inserts the subject and body text of the e-mail message into the Word document just after the smart tag-enabled text (Figure 5).

Figure 5. Document contents after inserting data from an e-mail message

E-mail message inserted into Word document

Of course, you can also alter this functionality including changing the table format, which field contents the code inserts into the document, and where it puts the field contents.

NoteNote

How to format Word content programmatically goes beyond the scope of this article, and you can find out more about how to program the Word object model by consulting the resources listed at the end of this article.

The main function of recognition in smart tag code is to compare textual input with some condition and determine if the text is important. For example, you can compare against a hard-coded list (the most inflexible but speediest mechanism), a dynamic list, or a regular expression. The sample for this article uses an XML file (Figure 1) containing terms that the smart tag DLL loads at runtime. The DLL stores the loaded term list in memory and uses it to compare against textual input. Here is the code to load the XML file and store the term list in memory:

  Public Sub SmartTagInitialize( _
  ByVal ApplicationName As String) _
  Implements SmartTags.ISmartTagRecognizer2. _
  SmartTagInitialize
    Dim xmlDoc As New Xml.XmlTextReader("SearchTerms.xml")
    While xmlDoc.Read
      If xmlDoc.NodeType = Xml.XmlNodeType.Text Then
        ReDim Preserve termList(termCount)
        termList(termCount) = xmlDoc.Value()
        termCount = termCount + 1
      End If
    End While
  End Sub

This code executes when the SmartTagInitialize event fires. You could also code your smart tag to load the list at other times or periodically check for updates all of which would require different but not difficult code.

Once the list loads, code in the Recognize or Recognize2 method can use it to compare against what a user has typed in the document.

NoteNote

When you implement the interfaces to create additional smart tag recognizers or action handlers in Office 2003 Editions, you can implement the legacy smart tag interfaces (version 1.0) or the new ones (verions 2.0). The Smart Tag Type Library 2.0 contains both interface versions. The Recognize method belongs to version 1.0, and the Recognize2 method belongs to version 2.0. Similarly, the InvokeVerb method for actions is for version 1.0 while InvokeVerb2 is for version 2.0. The code in this article uses the version 2.0 methods.

The recognizer focuses mainly on determining if a given string of text is relevant. You must code the logic for this according to your needs. Below is the logic for the Recognize2 method:

    Dim i As Integer
    Dim propbag As SmartTags.ISmartTagProperties
    Dim token As SmartTags.ISmartTagToken
    Try

      Dim nToken As Integer
      If Not TokenList Is Nothing Then
       For nToken = 1 To TokenList.Count
         token = TokenList.Item(nToken)
         If Not token Is Nothing Then
            For i = 0 To termCount - 1
              If token.Text.ToLower = termList(i).ToLower Then
               propbag = RecognizerSite2.GetNewPropertyBag
               RecognizerSite2.CommitSmartTag2( _
               SEARCH_NAMESPACE, _
               token.Start, token.Length, propbag)
              End If
            Next i
         End If
       Next
      End If
    Catch ex As Exception
      ' Add exception handling code
    End Try

This code loops through the TokenList collection passed as an argument of the Recognize2 method. The TokenList collection contains the text you want the code to evaluate. As you loop through the collection, you compare its items to the items you have stored in memory after reading the term list XML file.

When the code finds a match, it gets a new PropertyBag object and commits a smart tag, effectively marking up the recognized text with a namespace attribute. When you hover over the text in Word, the application knows that the text is marked up in this way and presents a menu as specified in the class that handles smart tag actions.

Recognition is only half of the smart tag technology. You also need to code the actions for the smart tag. This is where things are the most interesting because the action handler has code to provide functionality for what you want to happen based on the recognized text. In this example, the action is to take the recognized term and search within the user's Exchange inbox looking for items whose subject contains the term. The code displays a Windows form with a DataGrid containing the list of search results (Figure 4). Right-clicking a search result in the grid inserts the items body text into the Word document just after the smart tag text (Figure 5).

Following is the code

    Try
      Select Case VerbID
        Case 1
          If ApplicationName = "Word.Application.11" Then
            Dim rngWord As Word.Range = DirectCast(Target, Word.Range)
            Dim dv As 
            dv = UseWebDAV(rngWord.Text)
            If dv.Count > 0 Then
             ' Create a new instance of the Windows form
             ' with a . Add columns to the grid.
            End If
          End If
      End Select
    Catch ex As Exception
      ' Add exception handling code
    End Try

Most of this code is devoted to formatting the DataGrid (that code is shown and explained later on) whose data source is a DataView returned from a custom function, UseWebDAV. This procedure contains the code for querying Exchange.

NoteNote

The InvokeVerb2 method has an argument representing the Word Range object where the smart tag text resides: Target. You must cast this argument to variable declared as Range. This code uses DirectCast() because Target needs no conversion.

You can access the Exchange message store in a variety of ways including ADO, ADO.NET, WebDAV, or simple HTTP. This article demonstrates retrieving items through WebDAV. WebDAV is a protocol that extends HTTP 1.1 (see RFC 2616), and you can configure Microsoft Exchange Server 2003 or Microsoft Exchange Server 2000 to allow access to its data storage by using this protocol. Using WebDAV you send requests in XML format over HTTP, and Exchange responds by returning an XML stream.

NoteNote

A very close alternative to using WebDAV that requires similar query syntax is using the AdvancedSearch in the Outlook object model. For more information, see Microsoft Knowledge Base Article - 326244 How to use the AdvancedSearch method to search for an item in Outlook. I have not yet benchmarked the performance levels of each query approach. Of particular interest would be the performance of AdvancedSearch when Outlook in Cached Exchange mode. The main reason for using WebDAV in this article is to expose developers to this technique that allows for querying not only mail folders in Exchange but all types of folders that use the Exchange storage system, even beyond public folders. This includes SharePoint Portal Server 2001, for example. When using WebDAV and AdvancedSearch you need to know your DASL syntax. Sue Mosher, well-known Outlook MVP, documents the DASL schema names and corresponding Outlook field display names. Download documentation for DASL schema names.

In the example for this article, the process of querying by using WebDAV is in its own function to keep things more orderly. The function accepts the search term as a parameter, and it returns a DataView instance containing the results of the search.

Private Function UseWebDAV(ByVal term As String) As 
' Code goes here.
End Function

You need to declare some variables in the procedure. They are as follows:

    ' These variables are used for the WebDAV communication
    Dim Request As System.Net.HttpWebRequest
    Dim Response As System.Net.HttpWebResponse
    Dim RequestStream As System.IO.Stream
    Dim ResponseStream As System.IO.Stream
    Dim ResponseXmlDoc As System.Xml.XmlDocument
    Dim bytes() As Byte
    ' These variables are for handling security
    Dim MyCredentialCache As System.Net.CredentialCache
    Dim strPassword As String
    Dim strDomain As String
    Dim strUserName As String
    ' These variables are for working with search results
    Dim SubjectNodeList As System.Xml.XmlNodeList
    Dim SenderNodeList As System.Xml.XmlNodeList
    Dim BodyNodeList As System.Xml.XmlNodeList
    Dim URLNodeList As System.Xml.XmlNodeList
    Dim myDataSet As New DataSet()
    Dim myRow As DataRow

There are three sets of variables. The first set contains variables for handling the communication between the smart tag DLL and the Exchange server. The second set is for setting up the security authorization for the WebDAV request and response. The final set declares some XmlNodeList variables, a DataSet, and a DataRow. These are used to get the result set into a specific structure for the final Windows form DataGrid.

For authorization, you must assign valid values to the user name, password, and domain name variables. These are used when creating a CredentialCache instance that you pass along with the WebDAV request.

    strUserName = "my_user_name"
    strPassword = "my_password"
    strDomain = "my_domain"
    MyCredentialCache = New System.Net.CredentialCache
    MyCredentialCache.Add(New System.Uri(URL), "Basic", _
    New System.Net.NetworkCredential(strUserName, strPassword, strDomain))

Next, the code sets up the query definition. The query is in SQL-style syntax, but the FROM clause does not specify a table. Instead, the FROM clause specifies the traversal of a specific folder in the Exchange data store.

    Dim QUERY As String = "<?xml version=""1.0""?>" _
       & "<g:searchrequest xmlns:g=""DAV:"">" _
       & "<g:sql>SELECT ""urn:schemas:httpmail:subject"", " _
       & """urn:schemas:httpmail:from"", ""DAV:displayname"", " _
       & """urn:schemas:httpmail:textdescription"" " _
       & "FROM SCOPE('deep traversal of """ & URL & """') " _
       & "WHERE ""DAV:ishidden"" = False AND ""DAV:isfolder"" = False " _
       & "AND ""urn:schemas:httpmail:subject"" LIKE '%" & term & "%' " _
       & "ORDER BY ""urn:schemas:httpmail:date"" DESC" _
       & "</g:sql></g:searchrequest>"

This query does a deep traversal of the specific starting point in the person's inbox in Exchange. Here, the query uses a variable, URL, for this purpose. You declare the URL variable as a class-level variable like this:

Private Const URL As String = "http://my_server/exchange/my_user/inbox/"

The code then creates instances of HttpWebRequest and HttpWebResponse. You use these to send the request to the Exchange server and to get its response. The response will come in the format of an XML stream.

    Request = CType(System.Net.WebRequest.Create(URL), _
      System.Net.HttpWebRequest)
    Request.Credentials = New System.Net.NetworkCredential( _
    strUserName, strPassword, strDomain)    
    Request.Method = "SEARCH"
    Request.ContentType = "text/xml"
    bytes = System.Text.Encoding.UTF8.GetBytes(QUERY)
    Request.ContentLength = bytes.Length
    RequestStream = Request.GetRequestStream()
    RequestStream.Write(bytes, 0, bytes.Length)
    RequestStream.Close()
    Request.Headers.Add("Translate", "F")
    Response = Request.GetResponse()
    ResponseStream = Response.GetResponseStream()

Because the response comes as an XML stream, you can load it into an instance of XmlDocument. Then, you can separate out the different fields of interest. In this case, these are the subject, from, href, and textdescription fields. These come from different namespaces, so you need to use the proper namespace prefixes when calling the GetElementsByTagName method to load the elements in to XmlNodeList objects.

    ' Create the XmlDocument object from the XML response stream.
    ResponseXmlDoc = New System.Xml.XmlDocument()
    ResponseXmlDoc.Load(ResponseStream)
    SubjectNodeList = ResponseXmlDoc.GetElementsByTagName("d:subject")
    SenderNodeList = ResponseXmlDoc.GetElementsByTagName("d:from")
    URLNodeList = ResponseXmlDoc.GetElementsByTagName("a:href")
    BodyNodeList = _
     ResponseXmlDoc.GetElementsByTagName("d:textdescription")

If one of the XmlNodeList objects contains child elements you know that the search results are not empty. Then, you can add a new table to a DataSet instance and add new columns to the table.

    If SubjectNodeList.Count > 0 Then
      myDataSet.Tables.Add(New DataTable("Emails"))
      myDataSet.Tables("Emails").Columns.Add("Subject", _
        System.Type.GetType("System.String"))
      myDataSet.Tables("Emails").Columns.Add("From", _
        System.Type.GetType("System.String"))
      myDataSet.Tables("Emails").Columns.Add("URL", _
        System.Type.GetType("System.String"))
      myDataSet.Tables("Emails").Columns.Add("BODY", _
        System.Type.GetType("System.String"))

Looping through the XmlNodeList objects, you can add the text values of their elements to corresponding field locations in the DataSet's table.

      Dim i As Integer
      For i = 0 To SubjectNodeList.Count - 1
        myRow = myDataSet.Tables("Emails").NewRow()
        myRow("Subject") = SubjectNodeList(i).InnerText
        myRow("From") = SenderNodeList(i).InnerText
        myRow("URL") = URLNodeList(i).InnerText
        myRow("BODY") = BodyNodeList(i).InnerText
        myDataSet.Tables("Emails").Rows.Add(myRow)
      Next
    End If

As a matter of course, you should close the objects used to communicate with the Exchange server.

    ResponseStream.Close()
    Response.Close()

Finally, you return an instance of a DataView containing the table you just created.

    Dim dv As  = _
      New (myDataSet.Tables("Emails"))
    Return dv

Earlier, you saw that the code in the InvokeVerb2 method calls the custom procedure, UseWebDAV. This procedure returns a DataView instance that the code assigns as the DataGrid's data source. The DataGrid exists on a custom Windows form. You need to create an instance of this form and display it after configuring the grid. The code maps programmatically added DataGrid columns to columns in the DataView table. The width of two columns is set to zero so that those columns are not displayed.

If dv.Count > 0 Then
  ' Create a new instance of the Windows form
  ' with a DataGrid. Add columns to the grid.
    Dim f As New Form1
    Dim DataGridTextBoxColumn1 As _
    System.Windows.Forms.DataGridTextBoxColumn = _
    New System.Windows.Forms.DataGridTextBoxColumn
    Dim DataGridTextBoxColumn2 As _
    System.Windows.Forms.DataGridTextBoxColumn = _
    New System.Windows.Forms.DataGridTextBoxColumn
    Dim DataGridTextBoxColumn3 As _
    System.Windows.Forms.DataGridTextBoxColumn = _
    New System.Windows.Forms.DataGridTextBoxColumn
    Dim DataGridTextBoxColumn4 As _
    System.Windows.Forms.DataGridTextBoxColumn = _
    New System.Windows.Forms.DataGridTextBoxColumn
    Dim DataGridStyle As DataGridTableStyle = _
    New DataGridTableStyle
    DataGridStyle.MappingName = "Emails"
    DataGridStyle.GridColumnStyles.Add(DataGridTextBoxColumn1)
    f.DataGrid1.TableStyles.Add(DataGridStyle)
    DataGridTextBoxColumn1.MappingName = "Subject"
    DataGridTextBoxColumn1.HeaderText = "Subject"
    DataGridTextBoxColumn1.Width = 255
    DataGridTextBoxColumn2.MappingName = "From"
    DataGridTextBoxColumn2.HeaderText = "From"
    DataGridTextBoxColumn2.Width = 100
    DataGridTextBoxColumn3.MappingName = "URL"
    DataGridTextBoxColumn3.HeaderText = "URL"
    DataGridTextBoxColumn3.Width = 0
    DataGridTextBoxColumn4.MappingName = "BODY"
    DataGridTextBoxColumn4.HeaderText = "BODY"
    DataGridTextBoxColumn4.Width = 0
    f.DataGrid1.DataSource = dv
    f.WordRangeRef = rngWord
    f.ShowDialog()
End If

Here, the final line displays the form as a modally to reduce complexity, but you could display the data in the task pane or in another fashion. Closing the Windows form removes the form, the DataGrid, and the search results from memory, so clicking on the smart tag menu again requires an entirely new search, form instance, and reformatting of the grid. You could code things differently to cache search results or the form itself. Before displaying the form, the code gets a pointer to the Word Range object passed as an argument to the InvokeVerb2 method. It then passes this reference to the Windows form so that it can directly manipulate the text of the Word document. This allows code in the form to insert text in the Word document in response to an event.

When you hover over a selection in the DataGrid (Figure 6) and right-click, custom code executes to insert the body text for the target item into the Word document.

Figure 6. Search results in a DataGrid

Search results in a DataGrid

In order for this part of the solution to work you need code to respond to the right-click event and you need a reference to Word document so you can add text directly to it. To gain access to the Word document, the Windows form has a public property definition that you can set by code that creates an instance of the form. The property definition looks like this:

  Public WriteOnly Property WordRangeRef() _
    As Microsoft.Office.Interop.Word.Range
    Set(ByVal value As _
      Microsoft.Office.Interop.Word.Range)
      wrdRange = value
    End Set
  End Property

The wrdRange variable is a private, class-level variable in the Windows form class definition. The wrdRange object is only created by using the WordRangeRef property procedure which executes when calling code sets the property. You may recall that this property is set in the InvokeVerb2 method like this:

    f.WordRangeRef = rngWord

Code for the DataGrid's right-click event can access this instance of the Word Range and use it to add text to the target document. The code to respond to a user right-clicking the DataGrid is in its MouseDown event:

  Private Sub DataGrid1_MouseDown( _
    ByVal sender As Object, _
    ByVal e As System.Windows.Forms.MouseEventArgs) _
    Handles DataGrid1.MouseDown
    Dim dg As DataGridView = sender
    If e.Button = MouseButtons.Right Then
      Try
        Dim hti As DataGridView.HitTestInfo = dg.HitTest(e.X, e.Y)
        If hti.Type = 1 Then
          Dim dv As DataView
          dv = CType(DataGrid1.DataSource, DataView)
          Dim sel As _
          Microsoft.Office.Interop.Word.Selection = _
          wrdRange.Application.Selection
          wrdRange.InsertAfter(System.Environment.NewLine)
          wrdRange.Application.ActiveDocument.Tables.Add( _
          Range:=sel.Range, NumRows:=2, _
          NumColumns:=2, _
          DefaultTableBehavior:= _
          Word.WdDefaultTableBehavior.wdWord9TableBehavior, _
          AutoFitBehavior:=Word.WdAutoFitBehavior.wdAutoFitFixed)
          sel.Tables(1).Style = "Table Grid"
          sel.Tables(1).ApplyStyleHeadingRows = True
          sel.Tables(1).ApplyStyleLastRow = True
          sel.Tables(1).ApplyStyleFirstColumn = True
          sel.Tables(1).ApplyStyleLastColumn = True
          With wrdRange.Application.Selection
            .TypeText(Text:="Subject")
            .MoveRight(Unit:=Word.WdUnits.wdCell)
            .TypeText(Text:="Body")
            .MoveRight(Unit:=Word.WdUnits.wdCell)
            .TypeText(Text:=dv.Table.Rows.Item( _
              hti.RowIndex).Item(0).ToString())
            .MoveRight(Unit:=Word.WdUnits.wdCell)
            .TypeText(Text:=dv.Table.Rows.Item( _
              hti.RowIndex).Item(3).ToString())
          End With
          wrdRange.InsertAfter(System.Environment.NewLine)
        End If
      Catch ex As Exception
        MessageBox.Show(ex.Message, "Exception")
      End Try
    End If
  End Sub

Using an argument for the event, you can detect whether or not the user right-clicked the DataGrid. Then, you can determine where the user clicked using other event arguments. If the user has right-clicked a cell in the DataGrid, you want the code to continue. The first thing to do is get access to the data in DataView.

If e.Button = MouseButtons.Right Then
      Try
        Dim hti As DataGridView.HitTestInfo = dg.HitTest(e.X, e.Y)
        If hti.Type = 1 Then
          Dim dv As DataView
          dv = CType(DataGrid1.DataSource, DataView)
        ' Rest of the code goes here
      End Try
End If

The rest of the code creates a newly formatted Word table containing the subject and body text of the selected item in the DataGrid. The code inserts this Word table into the document:

          Dim sel As _
          Microsoft.Office.Interop.Word.Selection = _
          wrdRange.Application.Selection
          wrdRange.InsertAfter(System.Environment.NewLine)
          wrdRange.Application.ActiveDocument.Tables.Add( _
          Range:=sel.Range, NumRows:=2, _
          NumColumns:=2, _
          DefaultTableBehavior:= _
          Word.WdDefaultTableBehavior.wdWord9TableBehavior, _
          AutoFitBehavior:=Word.WdAutoFitBehavior.wdAutoFitFixed)
          sel.Tables(1).Style = "Table Grid"
          sel.Tables(1).ApplyStyleHeadingRows = True
          sel.Tables(1).ApplyStyleLastRow = True
          sel.Tables(1).ApplyStyleFirstColumn = True
          sel.Tables(1).ApplyStyleLastColumn = True
          With wrdRange.Application.Selection
            .TypeText(Text:="Subject")
            .MoveRight(Unit:=Word.WdUnits.wdCell)
            .TypeText(Text:="Body")
            .MoveRight(Unit:=Word.WdUnits.wdCell)
            .TypeText(Text:=dv.Table.Rows.Item( _
              hti.RowIndex).Item(0).ToString())
            .MoveRight(Unit:=Word.WdUnits.wdCell)
            .TypeText(Text:=dv.Table.Rows.Item( _
              hti.RowIndex).Item(3).ToString())
          End With
          wrdRange.InsertAfter(System.Environment.NewLine)

Because this solution is written in managed code, you may want to do a little extra maintenance with respect to the COM objects and how the .NET runtime manages their memory allocation. We release these objects from memory explicitly by calling a custom procedure in a shared class:

Friend Class GlobalUtil
  Public Shared Sub ReleaseComObjectInstance( _
    ByVal obj As Object)
    ' Clean up by releasing the objects
    Try
      Dim i As Integer
      Do
        i = System.Runtime.InteropServices. _
        Marshal.ReleaseComObject(obj)
      Loop While i > 0
    Catch ex As System.Exception
      MessageBox.Show("Exception in GlobalUtil")
      MessageBox.Show(ex.Message)
      MessageBox.Show(ex.StackTrace)
    Finally
      obj = Nothing
    End Try
  End Sub
End Class

You can then call this custom procedure in your code when you are finished using a COM object instance like this:

GlobalUtil.ReleaseComObjectInstance(Target)

Finally, you should add code to the click event of a button to close the Windows form when it is no longer needed.

Microsoft Office Word 2003 includes a number of built-in smart tags. Some of these let you take action with Outlook data using typed text as the point of departure. However, you can create your own smart tags to take different actions. This article shows how to create a smart tag that does programmatic searches of the Exchange message store looking for items containing terms a user has typed in a document. Ultimately, one of the goals of smart tags is to add value to the user's experience in the application. You can create your own smart tag recognizers and action handlers, and in so doing, you can bring the user's activity in documents into proximity with many other data sources and tasks. Smart tags extend the reach of user from the document to data bases, Web services, email, scheduling, messaging and so much more.

Show:
© 2014 Microsoft