Code & Seek

Bring Windows Desktop Search Into Visual Studio With Our Cool Add-In

Sergey Mishkovskiy

This article discusses:

  • Creating a Visual Studio add-in
  • Creating an add-in tool window
  • Developing with Windows Desktop Search
This article uses the following technologies:
Visual Studio 2005, Windows Desktop Search SDK

Code download available at:WDS2006_07.exe(255 KB)

Contents

Creating the Add-in
Creating the Tool Window
Options Dialog Page
Windows Desktop Search SDK
Background Queries and UI Updates
Putting It All Together
Installation and Deployment
Conclusion

A s computer hard drives get larger, so does the amount of information you hold onto. You end up with thousands of files and e-mail messages to plow through to find just the information you need. Thankfully, Windows® Desktop Search can help.

Windows Desktop Search is very straightforward. It indexes all of your documents, files, and e-mail messages, and any other data provided to it through extensibility mechanisms. As new items are added and changes are made to existing items, they're reindexed. The UI lets you search for strings and keywords in indexed items and open any found item with the associated app.

Windows Desktop Search provides an SDK that allows for its indexes and searching functionality to be consumed by other applications. As a developer, I spend a lot of time in Microsoft® Visual Studio®, and I spend a lot of time hunting around in my source code for material relevant to my development task at hand. For example, I'll search for other code snippets I previously authored using a particular class or e-mail conversations I had with a colleague concerning a certain collection's usage. By writing an add-in for Visual Studio that communicates with Windows Desktop Search, it's possible to construct a Dynamic Help-like tool window that uses Desktop Search to show relevant search results for the currently active text in Visual Studio. In this article, I'll show you how to build such an add-in. Download the code for this issue so you can follow along.

The project demonstrates two technologies: building an add-in for Visual Studio 2005 using extensibility interfaces, and integrating the add-in with Windows Desktop Search.

Visual Studio add-ins that present data in a non-modal fashion to the user normally consist of a tool window, at least one menu command, an Options dialog box to provide configuration alternatives, and an About dialog box. I want my add-in tool window to include Windows Desktop Search location filters, such as Everything, Files, and E-mail. I want the add-in to query automatically based on the selected text in the Visual Studio editor (see Figure 1), but also to include an option that turns this auto-refresh feature off. I want the query results to be displayed in a list view and to open with their associated applications when users double-click. Finally, I want to provide configuration capabilities through the Options dialog box. Let's first build the add-in and then move on to Windows Desktop Search integration.

Figure 1 Search Based on Select Text

Figure 1** Search Based on Select Text **

Creating the Add-in

Creating the add-in is a very straightforward process. Select the File | New | Project menu item. In the New Project dialog box, choose Other Project Types, then click Extensibility. Under Templates, select Visual Studio Add-in, give the new add-in a name, and click OK. The Visual Studio Add-in Wizard walks you through the rest. Choose C# as the programming language and Visual Studio 2005 as the host, add a name and description, then check the option to create a Tools menu item and add information for an automatically created About box.

After clicking the Finish button, you'll end up with an add-in skeleton to work with. Note that the project generator adds IDTExtensibility2 and IDTCommandTarget interface implementations for you, as well as a design time environment (DTE) extensibility application object reference.

Most of the Visual Studio tool window-related items appear under the View menu. To match that, change the Tools menu reference in the add-in to View, update its text to Windows Desktop Search, and move the generated menu item and command code into a method called CreateCommand.

The next step is to change how the code calls the AddNamedCommand2 command that creates the View item. Visual Studio provides a default image for the new menu item, but you may prefer to use your own image. For that to work, the image must reside in a satellite assembly. With previous versions of Visual Studio you had to create a separate project for the satellite assembly. With Visual Studio 2005 all it takes is adding a resource file to the project.

Add a resource item to the add-in with the name WDSAddin.en, then insert an icon resource named 1. Compiling the add-in project with the new resource automatically generates a resource assembly under the en folder. That resource ID 1 is what you use with the AddNamedCommand2 call to assign your own menu item image. Don't forget to change the AddNamedCommand2's MSOButton parameter value to false to indicate you want to use your own image.

After adding the command, you get a Command object reference back and can assign a keyboard shortcut to it. The Command interface has a Bindings property for just that purpose. It returns a collection of shortcuts assigned for all scopes. Scope would be something like Global, Text Editor, Windows Forms Designer, and so on. Be aware that scope names are localized.

The BindCommandKey method is responsible for assigning the keyboard shortcut. It includes a try/catch block around the code for setting shortcuts. Setting it up that way suppresses an exception raised when trying to change read-only keyboard scheme settings. Ctrl+Alt+Shift+M makes a good shortcut for the tool window as it mimics the Windows Desktop Search default Ctrl+Alt+M shortcut.

Notice that the generated add-in class code implements an IDTCommandTarget interface, which tells Visual Studio whether the command is available in the given context. That's done via the QueryStatus method. If the command is determined to be available to the user, then after it's selected, the Exec method gets called. With both methods, the command name gets checked to make sure it is in fact that command that needs to be processed. You use the Exec method to show the Windows Desktop Search tool window by calling into the private Show-WDSToolWindow method. This method checks to see if the tool window reference is set. If it is, the method shows the tool window by setting its Visible property to true. Otherwise the ShowWDSToolWindow method creates and shows a new tool window.

Creating the Tool Window

You used to have to rely on C++ shim controls to host managed user controls in the tool window. And there were known tool window state persistence issues, such as saving tool window position, size, and docked state. Microsoft received a lot of questions and feedback on that and the nice folks on the Visual Studio extensibility team listened. Visual Studio 2005 introduced a new Window2 interface with a CreateToolWindow2 method that lets you host managed user controls in the tool window. The last parameter of CreateToolWindow2 returns a reference to a managed user control.

To set up a tool window, first add a user control called ToolWindowUserControl to your project, then insert a top-aligned panel to serve as a toolbar. Add a client-aligned List view below the header panel for search results. Figure 2 shows the code that creates the tool window.

Figure 2 Creating the Tool Window

string assembly = this.GetType().Assembly.Location; string userControlClass = typeof(ToolWindowUserControl).FullName; Window2 appWin = (Window2)application.Windows; string toolWindowGuid = "{AB66652C-89AD-4229-B80C-C5B87545297F}"; object control = null; _toolWindow = appWin.CreateToolWindow2(_addin, assembly, userControlClass, "Windows Desktop Search", toolWindowGuid, ref control); _toolWindowControl = control as ToolWindowUserControl;

Notice that the CreateToolWindow2 method expects you to provide the user control assembly location and full class name. It also needs a GUID to associate the tool window with a Window class reference. This parameter can be any GUID value, but it needs to be generated once and kept the same afterwards for the tool window state persistence to work correctly. If the CreateToolWindow2 call succeeds, you get an object reference that can be safely cast back to the tool window user control class type.

With a tool window reference on hand, you can set the picture via a SetTabPicture call and show the tool window picture by setting the Enabled property to true. The Window.SetTabPicture method expects a GDI bitmap so add another resource file named Resources, and then add an icon resource named WDSIcon to that file. Since Visual Studio generates a resource wrapper class, retrieving the resource is very simple (the tool window image must be set before making the tool window visible):

_toolWindow.SetTabPicture(Resources.WDSIcon.ToBitmap().GetHbitmap()); _toolWindow.Visible = true;

Why not reuse the View command icon for the tool window? The transparency requirements for the command and tool window icons are different. Visual Studio command icons use a lime transparency color (0,255,0 RGB), whereas tool window icons use a magenta transparency color (255,0,255 RGB).

Common tool window creation practice is to create the tool window either at add-in load time or in response to command execution. After that you should keep the tool window reference around until the add-in gets unloaded, instead of creating a new tool window instance. You should also provide a View menu item to show the tool window in case the user closes it. When that happens, the tool window reference should still be live and the tool window can still be made visible.

Options Dialog Page

With the tool window in place, you can give the add-in some configuration options, as shown in Figure 3. First create a new class library called WDSToolsOptions, then add a new user control called OptionsPage to this class library.

Figure 3 Tools Options Dialog Page

Figure 3** Tools Options Dialog Page **

To add Options dialog box support, you need to implement an IDTToolsOptionsPage interface on the new user control—this is a fairly straightforward process. There are only two interface methods of interest here. The OnAfterCreated method reads previously saved configuration options from the registry and updates the UI accordingly. And the OnOK method saves UI options to the registry. You'll want to include a static Settings class to hold all configuration options to avoid frequent registry reads. You can see these in the code download for this issue.

The final step is to set the add-in to use your new options page assembly. There's no better place for that than the .addin XML configuration file, the new Visual Studio 2005 add-in deployment feature, which simplifies add-in installation and eliminates the need for registry-based registration. Here is all it takes to configure the Options page with your add-in:

<ToolsOptionsPage> <Category Name="Windows Desktop Search"> <SubCategory Name="General"> <Assembly>[Path-To-Addin]\WDSToolsOptions.dll</Assembly> <FullClassName>WDSToolsOptions.OptionsPage</FullClassName> </SubCategory> </Category> </ToolsOptionsPage>

As you can see, you simply tell Visual Studio what assembly to load and what class name within that assembly to use for the Options page. Note that it's a good practice to call the first sub-page General to follow the standard Microsoft page-naming convention.

The .addin file also contains all the About dialog information for the add-in, including an icon. The default icon created for the add-in is a binary encoded string, which makes it difficult to modify, so it's a good idea to use a resource file icon instead.

Remember the resource file you added when you first created the add-in? For the About dialog, you'll create a new 32×32 icon called AboutIcon and add it under the WDSAddin.en resource file. The .addin configuration file lets the icon be loaded from the satellite assembly and have its resource ID used instead of a binary encoded string:

<AboutIconData>@AboutIcon</AboutIconData>

Windows Desktop Search SDK

The information that Windows Desktop Search has indexed can be queried via a simple COM API. There are only two public methods on the ISearchDesktop interface: ExecuteSQLQuery and ExecuteQuery. ExecuteSQLQuery is used to execute well-formed SQL queries against the underlying index store. ExecuteQuery, on the other hand, is more user-friendly and its query string syntactically matches what's offered through the Windows Desktop Search query input box. The following shows how the ExecuteQuery method is defined:

HRESULT ExecuteQuery(LPCWSTR lpcwstrQuery, LPCWSTR lpcwstrColumn, LPCWSTR lpcwstrSort, LPCWSTR lpcwstrRestriction, Recordset **ppiRs);

The ExecuteQuery parameters are explained in Figure 4. As you can see, the first four parameters are various string input values that control the query and resultset, while the last parameter is the output resultset.

Figure 4 ExecuteQuery Parameters

Parameter Description
lpcwstrQuery The query as you would type it in the query input box.
lpcwstrColumn Columns to include in resultset.
lpcwstrSort Columns to sort the resultset by.
lpcwstrRestriction Restrictions to append via WHERE clauses in the Windows Desktop Search SELECT.
ppi Rs The resulting recordset.

Luckily, you don't have to worry about translating that into a managed call. The Windows Desktop Search SDK is available for download and includes two managed assemblies, one of which is found it WDSQuery.dll. This COM interop assembly wraps the relevant Windows Desktop Search COM classes and interfaces. Most importantly, it exposes a class called SearchDesktopClass that implements ISearchDesktop and is used to execute WDS queries. Rather than creating one instance of SearchDesktopClass and keeping it for all searches, the recommended usage pattern for this class is to create a new instance for each query, retrieving the results and allowing the garbage collector to clean up after it. The following shows how to use SearchDesktopClass in your add-in:

SearchDesktopClass wdsSearch = new SearchDesktopClass(); _Recordset result = wdsSearch.ExecuteQuery( "Secondary", "Rank, FileName, DisplayFolder, Url, PerceivedType", "Rank DESC, DocTitle", "Contains(PerceivedType,'document')");

The query specifies four elements. It is executed for the string "Secondary". The comma-separated columns start with Rank and end with PerceivedType. The columns to sort on are Rank and DocTitle. And finally the filter is set to retrieve all items of the 'document' type. This call essentially translates to a request for all documents that have the word "Secondary" in them.

Windows Desktop Search supports a number of built-in columns to query and sort on. Some of the common ones you can use are shown in Figure 5. Note that the results are intentionally sorted by Rank in descending order in the sample code. This ensures that the most probable results are at the top of the resultset.

Figure 5 Common Query and Sort Columns

Column Description
Rank Represents how well items match or score on query request. The higher the number, the more relevant the match is.
File name Name of the file without path.
DisplayFolder The file’s path.
Url Item path that can be used to run a shell execute command on the file.
PerceivedType The item type.

Background Queries and UI Updates

One of the early design goals was to build a tool that would have little impact on Visual Studio responsiveness. Windows Desktop Search runs index queries, and thus the queries execute very quickly. Still, depending on what you search for, a query may take anywhere from a few milliseconds to a second or more to execute. If queries are executed on the main Visual Studio thread, the UI will appear temporarily locked while the queries run. The obvious solution is to execute queries on a secondary thread.

You might consider using the new .NET Framework 2.0 BackgroundWorker class. Unfortunately, you can't. It turns out that this class is incompatible with SearchDesktopClass and its Windows Desktop Search COM object. Trying to execute a query using BackgroundWorker yields an InvalidCastException exception. In order to use SearchDesktopClass on a secondary thread, you must use a single-threaded apartment (STA) thread. BackgroundWorker uses the .NET ThreadPool, which by default contains multithreaded apartment (MTA) threads.

So add a new class to your add-in project called WDSQueryWorker that creates an STA thread using the Run method as a thread start delegate parameter. Run uses the WaitHandle class to wait on a single event handle, as shown here:

private void Run() { try { while (true) { WaitHandle.WaitAny(_runHandles); if (_stopped) break; ProcessEvent(); _runEvent.Reset(); } } finally { _stopped = true; } }

The WDSQueryWorker class also exposes a public method called DoWork that is used to set the event the Run method waits on, which in turn triggers query execution in the ProcessEvent method. Fields to query on should be set prior to calling DoWork.

The WDSQueryWorker class constructor takes three parameters: the UI control to call when your query completes, the query completion delegate, and the query error delegate. Both of the delegates are invoked via Control.Invoke. The Invoke method ensures that the delegate is executed on the control's UI thread. It's worth mentioning that arguments passed to the query completion delegate include the query _Resultset. Query error delegate arguments, on the other hand, include an Exception reference.

While moving query execution onto a separate thread takes care of some of the Visual Studio responsiveness issues associated with running large queries, there is one problem it doesn't address. As noted, the control's Invoke method executes a delegate on the control's UI thread, which happens to be the Visual Studio UI thread. If Windows Desktop Search returns a large resultset, it still takes time to populate the add-in's results List view. The obvious solution is to limit the displayed results. As a matter of fact, if you've enabled the Search-as-I-type option, Windows Desktop Search simply displays the first six results for each category, adding a "more..." item to show the rest. Using the Options dialog, you can set up the add-in to limit display results to, say, one hundred items per category by default. You can suppress this by setting the Display first results value to 0 if you prefer to see the entire resultset.

You'll want to make sure that as the user types code in the editor, even with limited query results, there is no delay caused by query execution. Luckily, Visual Studio extensibility provides editor events just for that. These events are accessible via the Events interface. Figure 6 shows how to set it up.

Figure 6 Hooking Editor Events

_editorEvents = _application.Events.get_TextEditorEvents(null); if (_editorEvents == null) throw new NullReferenceException( "Document events are not available."); _editorKeyPressEvents = ((Events2)_application.Events).get_TextDocumentKeyPressEvents(null); if (_editorKeyPressEvents == null) throw new NullReferenceException( "Document key press events are not available."); _editorEvents.LineChanged += new _dispTextEditorEvents_LineChangedEventHandler( _editorEvents_LineChanged); _editorKeyPressEvents.AfterKeyPress += new _dispTextDocumentKeyPressEvents_AfterKeyPressEventHandler( editorEvents_AfterKeyPress);

The idea is to delay queries if the user is entering code, scrolling through the code, or moving from one window to another. You can add your own delegates and set up all delegates for all these events to call into a private EditorUpdateInProgress method to delay the query execution.

Putting It All Together

When I set out to create this add-in, my goal was to have the query execute when the user selects some text in the Visual Studio editor, and originally I hoped to find some Visual Studio extensibility text selection event to work with. Though there is a TextSelection extensibility interface that returns the currently selected text, no text selection change events are exposed by the extensibility. This was an unfortunate setback. The only solution seems to be polling, which leads back to the issue of Visual Studio responsiveness. Polling is an inherently expensive operation because you end up polling when you don't necessarily need to. Thus, careful safeguards need to be put in place to prevent excessive polling.

One approach is to add a timer to your user control and set it to kick in every two seconds. You can also add several more less-aggressive timer-refresh settings to the Options dialog page. The timer's Tick event handler simply calls the private ExecuteQuery method, which is where the query is set up and secondary worker thread processing is initiated, as shown in Figure 7. Naming this method to match the Windows Desktop Search SDK's method name—SearchDesktopClass.ExecuteQuery—is a good idea, in order to emphasize its purpose.

Figure 7 Creating the Tool Window

private void ExecuteQuery() { _refreshing = true; bool clearResults = true, enabled = timerRefresh.Enabled; string currentDocument = string.Empty; timerRefresh.Stop(); try { // Check if auto-refresh has been disabled if (enabled && !Settings.AutoRefresh) return; // Sync up update frequency timerRefresh.Interval = GetOptionsUpdateFrequency(); // Get document’s selection Document activeDocument = _application.ActiveDocument; if (activeDocument != null) currentDocument = activeDocument.FullName; string selectedText = GetQueryText(activeDocument); if (selectedText == null) return; selectedText = selectedText.Trim(); // Selected text minimum length check if (selectedText.Length < Settings.MinimumLength) { _lastQuery = string.Empty; return; } // Unchanged search check if ((string.Compare(_lastQuery, selectedText, true) == 0) && (string.Compare(_lastDocument, currentDocument, true) == 0)) { clearResults = false; return; } // Save last query info _lastQuery = selectedText; _searching = true; _queryWorker.Query = selectedText; _queryWorker.DoWork(); } finally { // Update UI, save the last query, and restart the timer if (clearResults) ClearResults(); _lastDocument = currentDocument; if (Settings.AutoRefresh) timerRefresh.Start(); _refreshing = false; } }

When ExecuteQuery runs, the first thing it does is set a private processing field to true and disable the polling timer. It then retrieves the Visual Studio active document, if there is one, and retrieves its TextSelection, which has a Text property. This property returns the currently selected text in the editor.

Keep the last executed selection in a private field. After retrieving TextSelection.Text, check whether the currently selected text differs from the last text query. If they are the same, there's no point in re-executing and you can simply exit the method.

You should also check whether the selected text meets the minimum selected text requirement, as configured in the Options dialog page. You'll want the text selection to include at least three characters for the query to execute, a performance safeguard against a user selecting, for example, just the letter A and Windows Desktop Search returning thousands of entries.

If everything checks out, the query worker (the WDSQueryWorker class described earlier) is set up with the text to search for and its DoWork method is called. On the way out of the ExecuteQuery method, the timer is restarted (if it was previously running) and the processing field is set back to false. The results List view is cleared and the update status changed so "Searching..." is displayed.

The query worker's DoWork method sets an event, which causes it to execute a query. That's all done on a secondary thread, of course. When the query completes, it calls back into the main thread. The callback delegates happen to be on ToolWindowUserControl's side. One of them, the QueryCompleted method, simply calls DisplayResults to update the results List view. If the query fails, it calls into ToolWindowUserControl's QueryError method. Depending on the configuration in the Options dialog page, QueryError either displays an error message or silently ignores the error.

The DisplayResults method receives a single _Recordset parameter. If you recall, this is what the Windows Desktop Search SDK ExecuteQuery method returns. DisplayResults loops through the results fetching one row at a time. It retrieves column values based on a predefined set of fields to query on and updates the corresponding List view columns. It also groups items based on the PerceivedType column, which is used by Windows Desktop Search to identify item type. List view grouping is a new feature for the .NET Framework 2.0 and won't work on systems prior to Windows XP.

In addition to having a safeguard on minimum query string length, you should also restrict the number of results to display, perhaps to just the first 100 items per category. You can change this option to 0 to see the entire resultset. With a reasonable cap for the List view result, UI update processing occurs almost instantaneously, dramatically improving Visual Studio responsiveness. Play with the results option to see what works best for you.

One area for potential improvement is to see whether Windows Desktop Search supports limiting the resultset at the query level. That is, you might be able to speed the process if you moved implementation of the display-first-results option from trimming the display results to trimming the scope of the actual query.

Installation and Deployment

Visual Studio 2005 introduced a new registration-free deployment scheme based on .addin configuration files. This makes add-in installation as simple as copying assemblies to a designated folder and restarting Visual Studio. Here's the manual Windows Desktop Search add-in installation procedure:

  • Copy the contents of the WDSAddinBinaries folder to the \Program Files\WDSAddin folder. If you use a different folder, manually update the file path references in WDSAddin.addin.
  • Copy the WDSAddin.addin file to your \My Documents\Visual Studio 2005\Addins folder.
  • Make sure that a copy of Msvcp71.dll, also located in theWindows Desktop Search installation folder, exists somewhere on your Windows path. If that's not the case then simply copy that file from the Windows Desktop Search installation folder to \Windows\System32.

If you plan on debugging the Windows Desktop Search add-in, shut down Visual Studio and run the ToolWindow.reg file included in the code download at least once. It contains a workaround for Visual Studio 2005 add-in tool window state persistence issues. Otherwise, just skip this step.

Conclusion

The Windows Desktop Search SDK is rather simplistic at what it does and how it can be used, and the add-in I've described is not too complicated either. But the combination of the two is remarkably useful. Think of this add-in as analogous to Visual Studio Dynamic Help, but with the ability to scan your entire computer instead of just a predefined set of help files.

Sergey MishkovskiySergey Mishkovskiy is a Senior Software Developer at OpenSpan Inc. In his spare time he plays Xbox, reads, and works on a popular free Visual Studio add-in called DPack. To learn more about DPack, visit www.usysware.com/dpack. You can contact Sergey at mailto:sergey@usysware.com.