Walkthrough: Adding Search to a Tool Window
When you create or update a tool window in your extension, you can add the same search functionality that appears elsewhere in Visual Studio. This functionality includes the following features:
A search box that’s always located in a custom area of the toolbar.
A progress indicator that’s overlaid on the search box itself.
The ability to show results as soon as you enter each character (instant search) or only after you choose the Enter key (search on demand).
A list that shows terms for which you’ve searched most recently.
The ability to filter searches by specific fields or aspects of the search targets.
By following this walkthrough, you’ll learn how to perform the following tasks:
Create a VSPackage project.
Create a tool window that contains a UserControl with a read-only TextBox.
Add a search box to the tool window.
Add the search implementation.
Enable instant search and display of a progress bar.
Add a Match case option.
Add a Search even lines only filter.
To create a VSPackage project
On the menu bar, choose File, New, Project.
In the New Project dialog box, expand Other Project Types, and then choose Extensibility.
In the Templates pane, choose the Visual Studio Package template.
In the Name box, enter TestToolWindowSearch for the solution, and then choose the OK button.
The Visual Studio Package Wizard starts.
On the Select a Programming Language page, choose the Visual C# or Visual Basic option button, and then choose the Next button.
On the Basic VSPackage Information page, accept the default values, and then choose the Next button.
On the Select VSPackage Options page, select the Tool Window check box, ensure that all other check boxes are cleared, and then choose the Next button.
On the Tool Window Options page, enter TestSearchWindow in the Window name box and cmdidTestSearchWindow in the Command ID box, and then choose the Next button.
The value in the Window name box also appears on the menu command for your tool window. That command will appear on the Other Windows submenu of the View menu.
On the Select Test Project Options page, accept the default values, and then choose the Finish button to create your VSPackage.
To create a tool window
In the TestToolWindowSearch project, open the MyControl.xaml file.
Replace the existing <StackPanel> block with the following block, which adds a read-only TextBox to the UserControl in the tool window.
<StackPanel Orientation="Vertical"> <TextBox Name="resultsTextBox" Height="800.0" Width="800.0" IsReadOnly="True"> </TextBox> </StackPanel>
In the MyControl.xaml.cs or .vb file, remove the button1_Click() method.
In the MyControl class, add the following code.
This code adds a public TextBox property that’s named SearchResultsTextBox and a public string property that’s named SearchContent. In the constructor, SearchResultsTextBox is set to the text box, and SearchContent is initialized to a newline-delimited set of strings. The contents of the text box is also initialized to the set of strings.
Partial Public Class MyControl Inherits System.Windows.Controls.UserControl Public Property SearchResultsTextBox As System.Windows.Controls.TextBox Public Property SearchContent As String Public Sub New() ' This call is required by the designer. InitializeComponent() ' Add any initialization after the InitializeComponent() call. Me.SearchResultsTextBox = resultsTextBox Me.SearchContent = buildContent Me.SearchResultsTextBox.Text = Me.SearchContent End Sub Private Function BuildContent() As String Dim sb As New System.Text.StringBuilder() sb.AppendLine("1 go") sb.AppendLine("2 good") sb.AppendLine("3 Go") sb.AppendLine("4 Good") sb.AppendLine("5 goodbye") sb.AppendLine("6 Goodbye") Return sb.ToString() End Function End Class
public partial class MyControl : UserControl { public TextBox SearchResultsTextBox { get; set; } public string SearchContent { get; set; } public MyControl() { InitializeComponent(); this.SearchResultsTextBox = resultsTextBox; this.SearchContent = BuildContent(); this.SearchResultsTextBox.Text = this.SearchContent; } private string BuildContent() { StringBuilder sb = new StringBuilder(); sb.AppendLine("1 go"); sb.AppendLine("2 good"); sb.AppendLine("3 Go"); sb.AppendLine("4 Good"); sb.AppendLine("5 goodbye"); sb.AppendLine("6 Goodbye"); return sb.ToString(); } }
Build and debug the solution.
The experimental instance of Visual Studio appears.
On the menu bar, choose View, Other Windows, TestSearchWindow.
The tool window appears, but the search control doesn’t yet appear.
To add a search box to the tool window
In the MyToolWindow.cs or .vb file, add the following code to the MyToolWindow class. The code overrides the SearchEnabled property so that the get accessor returns true.
To enable search, you must override the SearchEnabled property. The ToolWindowPane class implements IVsWindowSearch and provides a default implementation that doesn’t enable search.
Public Overrides ReadOnly Property SearchEnabled As Boolean Get Return True End Get End Property
public override bool SearchEnabled { get { return true; } }
Rebuild and debug the solution.
In the experimental instance of Visual Studio, expand Other Windows, and then open TestSearchWindow.
At the top of the tool window, a search control appears with a Search watermark and a magnifying-glass icon. However, search doesn’t work yet because the search process hasn’t been implemented.
To add the search implementation
When you enable search on a ToolWindowPane, as in the previous procedure, the tool window creates a search host. This host sets up and manages search processes, which always occur on a background thread. Because the ToolWindowPane class manages the creation of the search host and the setting up of the search, you need only create a search task and provide the search method. The search process occurs on a background thread, and calls to the tool window control occur on the UI thread. Therefore, you must use the [M:Microsoft.VisualStudio.Shell.ThreadHelper.Invoke``1(System.Func`1)] method to manage any calls that you make in dealing with the control.
In the MyToolWindow.cs or .vb file, add the following using statements (C#) or Imports statements (Visual Basic):
Imports System Imports System.Collections.Generic Imports System.Runtime.InteropServices Imports System.Text Imports System.Windows.Controls Imports Microsoft.Internal.VisualStudio.PlatformUI Imports Microsoft.VisualStudio Imports Microsoft.VisualStudio.PlatformUI Imports Microsoft.VisualStudio.Shell Imports Microsoft.VisualStudio.Shell.Interop
using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; using System.Windows.Controls; using Microsoft.Internal.VisualStudio.PlatformUI; using Microsoft.VisualStudio; using Microsoft.VisualStudio.PlatformUI; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop;
In the MyToolWindow class, add the following code, which performs the following actions:
Overrides the CreateSearch method to create a search task.
Overrides the ClearSearch method to restore the state of the text box. This method is called when a user cancels a search task and when a user sets or unsets options or filters. Both CreateSearch and ClearSearch are called on the UI thread. Therefore, you don’t need to access the text box by means of the [M:Microsoft.VisualStudio.Shell.ThreadHelper.Invoke``1(System.Func`1)] method.
Creates a class that’s named TestSearchTask that inherits from VsSearchTask, which provides a default implementation of IVsSearchTask.
In TestSearchTask, the constructor sets a private field that references the tool window. To provide the search method, you override the OnStartSearch and OnStopSearch methods. The OnStartSearch method is where you implement the search process. This process includes performing the search, displaying the search results in the text box, and calling the base class implementation of this method to report that the search is complete.
Public Overrides Function CreateSearch(ByVal dwCookie As UInteger, ByVal pSearchQuery As IVsSearchQuery, ByVal pSearchCallback As IVsSearchCallback) As IVsSearchTask If pSearchQuery Is Nothing Or pSearchCallback Is Nothing Then Return Nothing Else Return New TestSearchTask(dwCookie, pSearchQuery, pSearchCallback, Me) End If End Function Public Overrides Sub ClearSearch() Dim control = DirectCast(Me.Content, MyControl) control.SearchResultsTextBox.Text = control.SearchContent End Sub Friend Class TestSearchTask Inherits VsSearchTask Private m_toolWindow As MyToolWindow Public Sub New(ByVal dwCookie As UInteger, ByVal pSearchQuery As IVsSearchQuery, ByVal pSearchCallback As IVsSearchCallback, ByVal toolwindow As MyToolWindow) MyBase.New(dwCookie, pSearchQuery, pSearchCallback) m_toolWindow = toolwindow End Sub Protected Overrides Sub OnStartSearch() ' Use the original content of the text box as the target of the search. Dim separator = New String() {Environment.NewLine} Dim contentArr As String() = DirectCast(m_toolWindow.Content, MyControl).SearchContent.Split(separator, StringSplitOptions.None) ' Get the search option. Dim matchCase As Boolean = False ' matchCase = m_toolWindow.MatchCaseOption.Value ' Set variables that are used in the finally block. Dim sb As New StringBuilder("") Dim resultCount As UInteger = 0 Me.ErrorCode = VSConstants.S_OK Try Dim searchString As String = Me.SearchQuery.SearchString ' Determine the results. Dim progress As UInteger = 0 For Each line As String In contentArr If matchCase = True Then If line.Contains(searchString) Then sb.AppendLine(line) resultCount += 1 End If Else If line.ToLower().Contains(searchString.ToLower()) Then sb.AppendLine(line) resultCount += 1 End If End If ' SearchCallback.ReportProgress(Me, progress, CUInt(contentArr.GetLength(0))) ' progress += 1 ' Uncomment the following line to demonstrate the progress bar. ' System.Threading.Thread.Sleep(100) Next Catch e As Exception Me.ErrorCode = VSConstants.E_FAIL Finally ThreadHelper.Generic.Invoke( Sub() DirectCast(DirectCast(m_toolWindow.Content, MyControl).SearchResultsTextBox, TextBox).Text = sb.ToString() End Sub) Me.SearchResults = resultCount End Try ' Call the implementation of this method in the base class. ' This sets the task status to complete and reports task completion. MyBase.OnStartSearch() End Sub Protected Overrides Sub OnStopSearch() Me.SearchResults = 0 End Sub End Class
public override IVsSearchTask CreateSearch(uint dwCookie, IVsSearchQuery pSearchQuery, IVsSearchCallback pSearchCallback) { if (pSearchQuery == null || pSearchCallback == null) return null; return new TestSearchTask(dwCookie, pSearchQuery, pSearchCallback, this); } public override void ClearSearch() { MyControl control = (MyControl)this.Content; control.SearchResultsTextBox.Text = control.SearchContent; } internal class TestSearchTask : VsSearchTask { private MyToolWindow m_toolWindow; public TestSearchTask(uint dwCookie, IVsSearchQuery pSearchQuery, IVsSearchCallback pSearchCallback, MyToolWindow toolwindow) : base(dwCookie, pSearchQuery, pSearchCallback) { m_toolWindow = toolwindow; } protected override void OnStartSearch() { // Use the original content of the text box as the target of the search. var separator = new string[] { Environment.NewLine }; string[] contentArr = ((MyControl)m_toolWindow.Content).SearchContent.Split(separator, StringSplitOptions.None); // Get the search option. bool matchCase = false; // matchCase = m_toolWindow.MatchCaseOption.Value; // Set variables that are used in the finally block. StringBuilder sb = new StringBuilder(""); uint resultCount = 0; this.ErrorCode = VSConstants.S_OK; try { string searchString = this.SearchQuery.SearchString; // Determine the results. uint progress = 0; foreach (string line in contentArr) { if (matchCase == true) { if (line.Contains(searchString)) { sb.AppendLine(line); resultCount++; } } else { if (line.ToLower().Contains(searchString.ToLower())) { sb.AppendLine(line); resultCount++; } } // SearchCallback.ReportProgress(this, progress++, (uint)contentArr.GetLength(0)); // Uncomment the following line to demonstrate the progress bar. // System.Threading.Thread.Sleep(100); } } catch (Exception e) { this.ErrorCode = VSConstants.E_FAIL; } finally { ThreadHelper.Generic.Invoke(() => { ((TextBox)((MyControl)m_toolWindow.Content).SearchResultsTextBox).Text = sb.ToString(); }); this.SearchResults = resultCount; } // Call the implementation of this method in the base class. // This sets the task status to complete and reports task completion. base.OnStartSearch(); } protected override void OnStopSearch() { this.SearchResults = 0; } }
Test your search implementation by performing the following steps:
Rebuild and debug the solution.
In the experimental instance of Visual Studio, open the tool window again, add text in the text box, enter search text in the search window, and then choose the Enter key.
The correct results should appear.
To customize the search behavior
By changing the search settings, you can make a variety of changes in how the search control appears and how the search is carried out. For example, you can change the watermark (the default text that appears in the search box), the minimum and maximum width of the search control, and whether to show a progress bar. You can also change the point at which search results start to appear (on demand or instant search) and whether to show a list of terms for which you recently searched. You can find the complete list of settings in the SearchSettingsDataSource class.
In the MyToolWindow.cs or .vb file, add the following code to the MyToolWindow class. This code enables instant search instead of on-demand search. The code overrides the ProvideSearchSettings method in the MyToolWindow class, which is necessary to change the default settings.
Public Overrides Sub ProvideSearchSettings(ByVal pSearchSettings As IVsUIDataSource) Utilities.SetValue(pSearchSettings, SearchSettingsDataSource.SearchStartTypeProperty.Name, CUInt(VSSEARCHSTARTTYPE.SST_INSTANT)) End Sub
public override void ProvideSearchSettings(IVsUIDataSource pSearchSettings) { Utilities.SetValue(pSearchSettings, SearchSettingsDataSource.SearchStartTypeProperty.Name, (uint)VSSEARCHSTARTTYPE.SST_INSTANT); }
Test the new setting by rebuilding the solution and restarting the debugger.
Search results appear every time that you enter a character in the search box.
In the ProvideSearchSettings method, add the following line, which enables the display of a progress bar.
Utilities.SetValue(pSearchSettings, SearchSettingsDataSource.SearchProgressTypeProperty.Name, CUInt(VSSEARCHPROGRESSTYPE.SPT_DETERMINATE))
Utilities.SetValue(pSearchSettings, SearchSettingsDataSource.SearchProgressTypeProperty.Name, (uint)VSSEARCHPROGRESSTYPE.SPT_DETERMINATE);
For the progress bar to appear, the progress must be reported. To report the progress, uncomment the following code in the OnStartSearch method of the TestSearchTask class:
SearchCallback.ReportProgress(Me, progress, CUInt(contentArr.GetLength(0))) progress += 1
SearchCallback.ReportProgress(this, progress++, (uint)contentArr.GetLength(0));
To slow processing enough that the progress bar is visible, uncomment the following line in the OnStartSearch method of the TestSearchTask class:
System.Threading.Thread.Sleep(100)
System.Threading.Thread.Sleep(100);
Test the new settings by rebuilding the solution and restarting the debugger.
The progress bar appears in the search window every time that you perform a search.
Comment out this line in the OnStartSearch method:
' System.Threading.Thread.Sleep(100)
// System.Threading.Thread.Sleep(100);
To enable users to refine their searches
You can allow users to refine their searches by means of options such as Match case or Match whole word. Options can be boolean, which appear as check boxes, or commands, which appear as buttons. For this walkthrough, you’ll create a boolean option.
In the MyToolWindow.cs or .vb file, add the following code to the MyToolWindow class. The code overrides the SearchOptionsEnum method, which allows the search implementation to detect whether a given option is on or off. The code in SearchOptionsEnum adds an option to match case to an IVsEnumWindowSearchOptions enumerator. The option to match case is also made available as the MatchCaseOption property.
Private m_optionsEnum As IVsEnumWindowSearchOptions Public Overrides ReadOnly Property SearchOptionsEnum() As IVsEnumWindowSearchOptions Get If m_optionsEnum Is Nothing Then Dim list As New List(Of IVsWindowSearchOption)() list.Add(Me.MatchCaseOption) m_optionsEnum = TryCast(New WindowSearchOptionEnumerator(list), IVsEnumWindowSearchOptions) End If Return m_optionsEnum End Get End Property Private m_matchCaseOption As WindowSearchBooleanOption Public ReadOnly Property MatchCaseOption Get If m_matchCaseOption Is Nothing Then m_matchCaseOption = New WindowSearchBooleanOption("Match case", "Match case", False) End If Return m_matchCaseOption End Get End Property
private IVsEnumWindowSearchOptions m_optionsEnum; public override IVsEnumWindowSearchOptions SearchOptionsEnum { get { if (m_optionsEnum == null) { List<IVsWindowSearchOption> list = new List<IVsWindowSearchOption>(); list.Add(this.MatchCaseOption); m_optionsEnum = new WindowSearchOptionEnumerator(list) as IVsEnumWindowSearchOptions; } return m_optionsEnum; } } private WindowSearchBooleanOption m_matchCaseOption; public WindowSearchBooleanOption MatchCaseOption { get { if (m_matchCaseOption == null) { m_matchCaseOption = new WindowSearchBooleanOption("Match case", "Match case", false); } return m_matchCaseOption; } }
In the TestSearchTask class, uncomment the following line in the OnStartSearch method:
matchCase = m_toolWindow.MatchCaseOption.Value
matchCase = m_toolWindow.MatchCaseOption.Value;
Test the option by performing the following steps:.
Rebuild the solution, and restart the debugger.
In the tool window, choose the Down arrow on the search control.
The Match case check box appears.
Select the Match case check box, and then perform some searches.
To add a search filter
You can add search filters that allow users to refine the set of search targets. For example, you can filter files in File Explorer by the dates on which they were modified most recently and their file name extensions. In this walkthrough, you’ll add a filter for even lines only. When the user chooses that filter, the search host adds the strings that you specify to the search query. You can then identify these strings inside your search method and filter the search targets accordingly.
In the MyToolWindow.cs or .vb file, add the following code to the MyToolWindow class. The code implements SearchFiltersEnum in the MyToolWindow class by adding a WindowSearchSimpleFilter that specifies to filter the search results so that only even lines appear.
Public Overrides ReadOnly Property SearchFiltersEnum() As IVsEnumWindowSearchFilters Get Dim list As New List(Of IVsWindowSearchFilter)() list.Add(New WindowSearchSimpleFilter("Search even lines only", "Search even lines only", "lines", "even")) Return TryCast(New WindowSearchFilterEnumerator(list), IVsEnumWindowSearchFilters) End Get End Property
public override IVsEnumWindowSearchFilters SearchFiltersEnum { get { List<IVsWindowSearchFilter> list = new List<IVsWindowSearchFilter>(); list.Add(new WindowSearchSimpleFilter("Search even lines only", "Search even lines only", "lines", "even")); return new WindowSearchFilterEnumerator(list) as IVsEnumWindowSearchFilters; } }
Now the search control displays the search filter Search even lines only. When the user chooses the filter, the string lines:"even" appears in the search box. Other search criteria can appear at the same time as the filter. Search strings may appear before the filter, after the filter, or both.
In the MyToolWindow.cs or .vb file, add the following methods to the TestSearchTask class, which is in the MyToolWindow class. These methods support the OnStartSearch method, which you’ll modify in the next step.
Private Function RemoveFromString(ByVal origString As String, ByVal stringToRemove As String) As String Dim index As Integer = origString.IndexOf(stringToRemove) If index = -1 Then Return origString Else Return origString.Substring(0, index) & origString.Substring(index + stringToRemove.Length) End If End Function Private Function GetEvenItems(ByVal contentArr As String()) As String() Dim length As Integer = contentArr.Length \ 2 Dim evenContentArr = New String(length) {} Dim indexB = 0 For index = 1 To contentArr.Length - 1 Step 2 evenContentArr(indexB) = contentArr(index) indexB += 1 Next Return evenContentArr End Function
private string RemoveFromString(string origString, string stringToRemove) { int index = origString.IndexOf(stringToRemove); if (index == -1) return origString; else return origString.Substring(0, index) + origString.Substring(index + stringToRemove.Length); } private string[] GetEvenItems(string[] contentArr) { int length = contentArr.Length / 2; string[] evenContentArr = new string[length]; int indexB = 0; for (int index = 1; index < contentArr.Length; index += 2) { evenContentArr[indexB] = contentArr[index]; indexB++; } return evenContentArr; }
In the TestSearchTask class, update the OnStartSearch method with the following code. This change updates the code to support the filter.
Protected Overrides Sub OnStartSearch() ' Use the original content of the text box as the target of the search. Dim separator = New String() {Environment.NewLine} Dim contentArr As String() = DirectCast(m_toolWindow.Content, MyControl).SearchContent.Split(separator, StringSplitOptions.None) ' Get the search option. Dim matchCase As Boolean = False matchCase = m_toolWindow.MatchCaseOption.Value ' Set variables that are used in the finally block. Dim sb As New StringBuilder("") Dim resultCount As UInteger = 0 Me.ErrorCode = VSConstants.S_OK Try Dim searchString As String = Me.SearchQuery.SearchString ' If the search string contains the filter string, filter the content array. Dim filterString = "lines:""even""" If searchString.Contains(filterString) Then ' Retain only the even items in the array. contentArr = GetEvenItems(contentArr) ' Remove 'lines:"even"' from the search string. searchString = RemoveFromString(searchString, filterString) End If ' Determine the results. Dim progress As UInteger = 0 For Each line As String In contentArr If matchCase = True Then If line.Contains(searchString) Then sb.AppendLine(line) resultCount += 1 End If Else If line.ToLower().Contains(searchString.ToLower()) Then sb.AppendLine(line) resultCount += 1 End If End If SearchCallback.ReportProgress(Me, progress, CUInt(contentArr.GetLength(0))) progress += 1 ' Uncomment the following line to demonstrate the progress bar. ' System.Threading.Thread.Sleep(100) Next Catch e As Exception Me.ErrorCode = VSConstants.E_FAIL Finally ThreadHelper.Generic.Invoke( Sub() DirectCast(DirectCast(m_toolWindow.Content, MyControl).SearchResultsTextBox, TextBox).Text = sb.ToString() End Sub) Me.SearchResults = resultCount End Try ' Call the implementation of this method in the base class. ' This sets the task status to complete and reports task completion. MyBase.OnStartSearch() End Sub
protected override void OnStartSearch() { // Use the original content of the text box as the target of the search. var separator = new string[] { Environment.NewLine }; string[] contentArr = ((MyControl)m_toolWindow.Content).SearchContent.Split(separator, StringSplitOptions.None); // Get the search option. bool matchCase = false; matchCase = m_toolWindow.MatchCaseOption.Value; // Set variables that are used in the finally block. StringBuilder sb = new StringBuilder(""); uint resultCount = 0; this.ErrorCode = VSConstants.S_OK; try { string searchString = this.SearchQuery.SearchString; // If the search string contains the filter string, filter the content array. string filterString = "lines:\"even\""; if (this.SearchQuery.SearchString.Contains(filterString)) { // Retain only the even items in the array. contentArr = GetEvenItems(contentArr); // Remove 'lines:"even"' from the search string. searchString = RemoveFromString(searchString, filterString); } // Determine the results. uint progress = 0; foreach (string line in contentArr) { if (matchCase == true) { if (line.Contains(searchString)) { sb.AppendLine(line); resultCount++; } } else { if (line.ToLower().Contains(searchString.ToLower())) { sb.AppendLine(line); resultCount++; } } SearchCallback.ReportProgress(this, progress++, (uint)contentArr.GetLength(0)); // Uncomment the following line to demonstrate the progress bar. // System.Threading.Thread.Sleep(100); } } catch (Exception e) { this.ErrorCode = VSConstants.E_FAIL; } finally { ThreadHelper.Generic.Invoke(() => { ((TextBox)((MyControl)m_toolWindow.Content).SearchResultsTextBox).Text = sb.ToString(); }); this.SearchResults = resultCount; } // Call the implementation of this method in the base class. // This sets the task status to complete and reports task completion. base.OnStartSearch(); }
If you want to test your code, rebuild the solution, restart the debugger, and then perform the remaining steps in this procedure.
In the experimental instance of Visual Studio, open the tool window, and then choose the Down arrow on the search control.
The Match case check box and the Search even lines only filter appear.
Choose the filter.
The search box contains lines:”even”, and the following results appear:
2 good
4 Good
6 Goodbye
Delete lines:"even" from the search box, select the Match case check box, and then enter g in the search box.
The following results appear:
1 go
2 good
5 goodbye
Choose the X on the right side of the search box.
The search is cleared, and the original contents appear. However, the Match case check box is still selected.
See Also
Tasks
Walkthrough: Creating a Menu Command By Using the Visual Studio Package Template