Search Inbox Data Using Smart Tags in Word 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
Applies to: Microsoft Office Word 2003, Microsoft Exchange Server 2003, Microsoft Exchange Server 2000
How It Works
Coding the Action Handler
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.
How It Works
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
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
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
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
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
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.
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.
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.
Coding the Action Handler
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.
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.
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.
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
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:
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.
This section lists a number of resources you can use to learn more about the products and technologies mentioned or used in this article.
Tools & Utilities: