Implementing search in a Windows Store business app using C#, XAML, and Prism
[This article is for Windows 8.x and Windows Phone 8.x developers writing Windows Runtime apps. If you’re developing for Windows 10, see the latest documentation]
From: Developing a Windows Store business app using C#, XAML, and Prism for the Windows Runtime
Learn how to search app content and provide query suggestions by adding a search box to your app canvas. The AdventureWorks Shopper reference implementation uses Prism for the Windows Runtime to implement the search functionality as a single user control and accompanying view model class that can be reused throughout the app.
Download
After you download the code, see Getting started using Prism for the Windows Runtime for instructions on how to compile and run the reference implementation, as well as understand the Microsoft Visual Studio solution structure.
You will learn
- How to use the SearchBox control to implement search functionality in a Windows Store app.
- How to provide query suggestions that help the user to search quickly.
- How to populate the search results page with results.
- How to navigate to the result's detail page.
- How to search for content in the app by typing directly into the search box, without selecting it first.
Applies to
- Windows Runtime for Windows 8.1
- C#
- Extensible Application Markup Language (XAML)
Making key decisions
When you add a search box to your app, users can search your app’s content from within the app. The following list summarizes the decisions to make when implementing search in your app:
- How should I include search functionality in my app?
- Should I provide query and result suggestions?
- Should I add a search icon to the app canvas?
- What should I display on my search results page?
You should use the SearchBox control to let users search for content in your app, in order to ensure that they have a consistent and predictable experience when they search. Regardless of where your app’s content is located, you can use the search box to respond to user’s queries and display search results in an app page of your own design.
When users start typing a query into a search box, apps can provide search suggestions beneath the search box. An app can provide two types of search suggestions: query suggestions and result suggestions. Query suggestions can be used as a way to auto complete query text that users can search for in your app, helping them search quickly by reducing the amount of typing needed to complete a search. Result suggestions can be used to directly take the user to the details of a result without first taking them to a search results page.
A search box is a great way for users to know where to start searching. However, if space is a concern for your layout, you should use an icon that expands to reveal a search box.
When users submit a search query to your app, they see a page that shows search results for the query. You design the search results page for your app, and so must ensure that the presented results are useful and have an appropriate layout. You should use a grid layout to display search results, and let users see their query text on the page. Also, you should indicate why a search result matches the query by highlighting the user's query in each result, which is known as hit highlighting. In addition, you should let users navigate back to the last-viewed page after they look at the details for a search result. This can be accomplished by including a back button in the app's UI. This back button should be used to go to the page that the user was interacting with before they submitted their search. Your app could also provide a mechanism to exit the search results page, such as a top app bar button that performs navigation.
For more info see Guidelines for search.
[Top]
Search in AdventureWorks Shopper
The AdventureWorks Shopper reference implementation uses the SearchBox control to respond to user's queries and display search results in an app page. When a user starts typing on a page that contains a search box the keyboard input is automatically captured by the search box. As the user enters a query, a maximum of 5 query suggestions are provided as a way to auto complete the query text that the user is searching for.
Search results are displayed using the AutoRotatingGridView custom control. The search results page includes the user's query text, hit highlighting to indicate why a search result matches the query, and lets users navigate back to the last-viewed page and to the HubPage and the ShoppingCartPage. For more info see Adding search functionality.
AdventureWorks Shopper includes a search box on the app canvas for the HubPage, CategoryPage, GroupDetailPage, ItemDetailPage, and SearchResultsPage. When the app is in the portrait or minimal view state the search box is shown in a compact state. The search box is prominently located next to the shopping cart icon, as shown in the following diagram. For more info see Adding search functionality.
[Top]
Adding search functionality
The SearchUserControl class defines the SearchBox control that’s added to the app canvas on the HubPage, CategoryPage, GroupDetailPage, ItemDetailPage, and SearchResultsPage. This approach allows the search functionality to reside in a single user control and accompanying view model class, rather than having to be repeated across the classes for each page.
AdventureWorks.Shopper\Views\SearchUserControl.xaml
<SearchBox x:Name="searchBox"
Height="40"
x:Uid="SearchBoxUserControl"
PlaceholderText="Search for a Product"
VerticalAlignment="Center"
SearchHistoryEnabled="False"
Padding="10,10,0,0" >
<interactivity:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="QuerySubmitted">
<core:InvokeCommandAction Command="{Binding SearchCommand}" />
</core:EventTriggerBehavior>
<core:EventTriggerBehavior EventName="SuggestionsRequested">
<core:InvokeCommandAction Command="{Binding SearchSuggestionsCommand}" />
</core:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
</SearchBox>
The SearchBox control is used to enable search in an app by letting the user enter queries and by displaying suggestions. Placeholder text is shown in the search box, to describe what users can search for in AdventureWorks Shopper. The text is only shown when the search box is empty, and is cleared if the user starts typing into the box. This is accomplished by setting the PlaceholderText property of the SearchBox class. In addition, the SearchHistoryEnabled property has been set to false in order to disable search history suggestions for queries. This helps to reduce confusion over whether a result is a search history suggestion or a query suggestion.
Blend for Microsoft Visual Studio 2013 behaviors are used to invoke view model commands in response to events being raised on the SearchBox. When the user submits a search query the QuerySubmitted event is raised by the SearchBox, and the SearchCommand in the SearchUserControlViewModel is executed. For more info see Responding to search queries. When the user’s query text changes and the app needs to provide new query suggestions the SuggestionsRequested event is raised by the SearchBox, and the SearchSuggestionsCommand in the SearchUserControlViewModel is executed.
The SearchSuggestionRepository class, in the AdventureWorks.WebServices project, provides query suggestions to help the user quickly search the app. This class is called by the ProductRepository class to retrieve the query suggestions that are used to populate the search suggestions in the search pane. For more info see Providing query suggestions.
The SearchResultsPage includes a back button and a top app bar that allows users to navigate to the HubPage and the ShoppingCartPage. If AdventureWorks Shopper is suspended while the SearchResultsPage is active, the app will correctly restore page state upon reactivation by using the Microsoft.Practices.Prism.StoreApps library. This includes the AutoRotatingGridView scroll position, the user's query text, and the search results. This avoids the need to requery the data using the query text. For more info see Populating the search results page with data.
For more info see Adding search to an app and Quickstart: Adding search to an app.
Providing query suggestions
When the user’s query text changes and the app needs to provide new query suggestions the SuggestionsRequested event is raised by the SearchBox, and the SearchSuggestionsCommand in the SearchUserControlViewModel is executed. This in turn executes the SearchBoxSuggestionsRequested method, which retrieves query suggestions from the AdventureWorks Shopper web service, and is shown in the following code example.
AdventureWorks.UILogic\ViewModels\SearchUserControlViewModel.cs
private async Task SearchBoxSuggestionsRequested(SearchBoxSuggestionsRequestedEventArgs args)
{
var queryText = args.QueryText != null ? args.QueryText.Trim() : null;
if (string.IsNullOrEmpty(queryText)) return;
var deferral = args.Request.GetDeferral();
try
{
var suggestionCollection = args.Request.SearchSuggestionCollection;
var querySuggestions = await _productCatalogRepository.GetSearchSuggestionsAsync(queryText);
if (querySuggestions != null && querySuggestions.Count > 0)
{
var querySuggestionCount = 0;
foreach (string suggestion in querySuggestions)
{
querySuggestionCount++;
suggestionCollection.AppendQuerySuggestion(suggestion);
if (querySuggestionCount >= MaxNumberOfSuggestions)
{
break;
}
}
}
}
catch (Exception)
{
//Ignore any exceptions that occur trying to find search suggestions.
}
deferral.Complete();
}
The SearchUserControlViewModel class provides the suggestionCollection variable that’s used to populate the query suggestions. This variable is populated with a list of query suggestions by the ProductCatalogRepository instance, which gets the query suggestions from the SearchSuggestionRepository class in the AdventureWorks.WebServices project. The AppendQuerySuggestion method then appends any query suggestions from the suggestionCollection variable that match the user’s query to the list of search suggestions shown below the SearchBox. The MaxNumberOfSuggestions constant is used to limit the number of query suggestions that is shown to 5.
For more info see Quickstart: Adding search to an app.
Responding to search queries
When the user submits a search query the QuerySubmitted event is raised by the SearchBox, and the SearchCommand in the SearchUserControlViewModel is executed. This in turn executes the SearchBoxQuerySubmitted method, which is shown in the following code example.
AdventureWorks.UILogic\ViewModels\SearchUserControlViewModel.cs
private void SearchBoxQuerySubmitted(SearchBoxQuerySubmittedEventArgs eventArgs)
{
var searchTerm = eventArgs.QueryText != null ? eventArgs.QueryText.Trim() : null;
if (!string.IsNullOrEmpty(searchTerm))
{
_navigationService.Navigate("SearchResults", searchTerm);
}
}
This method responds to the QuerySubmitted event by navigating to the SearchResultsPage with the user’s query provided that the query contains data.
Populating the search results page with data
When users search AdventureWorks Shopper the SearchResultsPage is used to display search results. The OnNavigatedTo method in the SearchResultsPageViewModel class is used to populate the page with the search results, as shown in the following code example.
AdventureWorks.UILogic\ViewModels\SearchResultsPageViewModel.cs
public async override void OnNavigatedTo(object navigationParameter, NavigationMode navigationMode, Dictionary<string, object> viewModelState)
{
base.OnNavigatedTo(navigationParameter, navigationMode, viewModelState);
var queryText = navigationParameter as String;
string errorMessage = string.Empty;
this.SearchTerm = queryText;
this.QueryText = '\u201c' + queryText + '\u201d';
try
{
ReadOnlyCollection<Product> products;
if (queryText == PreviousSearchTerm)
{
products = PreviousResults;
}
else
{
var searchResults = await _productCatalogRepository.GetFilteredProductsAsync(queryText, 0);
products = searchResults.Products;
TotalCount = searchResults.TotalCount;
PreviousResults = products;
}
var productViewModels = new List<ProductViewModel>();
foreach (var product in products)
{
productViewModels.Add(new ProductViewModel(product));
}
// Communicate results through the view model
this.Results = new ReadOnlyCollection<ProductViewModel>(productViewModels);
this.NoResults = !this.Results.Any();
// Update VM status
PreviousSearchTerm = SearchTerm;
}
catch (Exception ex)
{
errorMessage = string.Format(CultureInfo.CurrentCulture, _resourceLoader.GetString("GeneralServiceErrorMessage"), Environment.NewLine, ex.Message);
}
if (!string.IsNullOrWhiteSpace(errorMessage))
{
await _alertMessageService.ShowAsync(errorMessage, _resourceLoader.GetString("ErrorServiceUnreachable"));
}
}
This method uses the ProductCatalogRepository instance to retrieve products from the web service if they match the queryText parameter, and store them in the Results property for display by the SearchResultsPage. If no results are returned by the ProductCatalogRepository, the NoResults property is set to true and the SearchResultsPage displays a message indicating that no products match your search. The method also saves the search results for the last query in case the user searches for that query again. This handles the scenario whereby the user might submit a search query to AdventureWorks Shopper, select an item from the search results, and then navigate back to the search results. This approach avoids retrieving a new set of search results, instead loading the previous search results.
For more info see Guidelines for search.
Navigating to the result's detail page
The ItemClick event of the AutoRotatingGridView custom control in the SearchResultsPage is used to invoke page navigation to the ItemDetailPage, in order to display detailed information about a user selected result.
AdventureWorks.Shopper\Views\SearchResultsPage.xaml
<awcontrols:AutoRotatingGridView x:Name="itemsGridView"
AutomationProperties.AutomationId="ResultsGridView"
AutomationProperties.Name="Search Results"
TabIndex="1"
Grid.Row="1"
Padding="100,0,80,50"
SelectionMode="None"
IsItemClickEnabled="True"
ItemsSource="{Binding Results}"
ItemTemplate="{StaticResource SearchResultsTemplate}"
MinimalItemTemplate="{StaticResource SearchResultsTemplateMinimal}"
Loaded="itemsGridView_Loaded">
<interactivity:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="ItemClick">
<awbehaviors:NavigateWithEventArgsToPageAction TargetPage="AdventureWorks.Shopper.Views.ItemDetailPage" EventArgsParameterPath="ClickedItem.ProductNumber"/>
</core:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
The EventTriggerBehavior binds the ItemClick event of the AutoRotatingGridView custom control to the NavigateWithEventArgsToPageAction. So when a GridViewItem is selected the NavigateWithEventArgsToPageAction is executed, which navigates from the SearchResultsPage to the ItemDetailPage, passing in the ProductNumber of the ClickedItem to the ItemDetailPage.
For more info about using Blend behaviors to invoke page navigation, see Implementing behaviors to supplement the functionality of XAML elements.
Enabling users to type into the search box
The AdventureWorks Shopper reference implementation provides the ability to search for content in the app by typing without selecting the search box first. This feature is known as type to search. Enabling type to search makes efficient use of keyboard interaction and makes the app's search experience consistent with the Start screen.
Type to search is enabled in AdventureWorks Shopper for the pages on which the SearchUserControl is displayed, and is controlled through the FocusOnKeyboardInput property of the SearchBox control. However, type to search will be disabled when the SignInFlyout is displayed in order to allow the user to enter their login credentials.
The SearchBox control in the SearchUserControl class uses the EnableFocusOnKeyboardInput, DisableFocusOnKeyboardInput, and FocusOnKeyboardInputToggle methods to set the FocusOnKeyboardInput property, in order to control whether the SearchBox receives input when users type.
AdventureWorks.Shopper\Views\SearchUserControl.xaml.cs
public void EnableFocusOnKeyboardInput()
{
this.searchBox.FocusOnKeyboardInput = true;
if (_eventAggregator != null)
{
_eventAggregator.GetEvent<FocusOnKeyboardInputChangedEvent>().Subscribe(FocusOnKeyboardInputToggle);
}
}
public void DisableFocusOnKeyboardInput()
{
this.searchBox.FocusOnKeyboardInput = false;
if (_eventAggregator != null)
{
_eventAggregator.GetEvent<FocusOnKeyboardInputChangedEvent>().Unsubscribe(FocusOnKeyboardInputToggle);
}
}
private void FocusOnKeyboardInputToggle(bool value)
{
this.searchBox.FocusOnKeyboardInput = value;
}
There can only be one SearchBox control with FocusOnKeyboardInput behavior enabled per thread. Because there are multiple instances of the SearchBox control in AdventureWorks Shopper, all with FocusOnKeyboardInput behavior enabled, then the last control to be enabled will gain keyboard focus while the other controls will no longer receive FocusOnKeyboardInput behavior. In order to ensure consistent behavior across all SearchBox controls in an app it is necessary to disable the FocusOnKeyboardInput behavior for a page’s SearchBox before navigating away from the page, and enable it while navigating to a page. Therefore, the EnableFocusOnKeyboardInput method is invoked from the OnNavigatedTo method of any page that uses the SearchUserControl, with the DisableFocusOnKeyboardInput method being invoked from the OnNavigatedFrom method of any page that uses the SearchUserControl.
The EnableFocusOnKeyboardInput and DisableFocusOnKeyboardInput methods also use event aggregation to subscribe to the FocusOnKeyboardInputEvent, providing a handler that will be invoked when the event is published. The purpose of this event is to allow AdventureWorks Shopper to disable type to search when showing the SignInFlyout, and restore it when the SignInFlyout closes.
AdventureWorks.Shopper\Views\SignInFlyout.xaml.cs
public SignInFlyout(IEventAggregator eventAggregator)
{
...
_eventAggregator.GetEvent<FocusOnKeyboardInputChangedEvent>().Publish(false);
...
}
void SignInFlyout_Unloaded(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
_eventAggregator.GetEvent<FocusOnKeyboardInputChangedEvent>().Publish(true);
}
When the SignInFlyout is shown, its constructor publishes the FocusOnKeyboardInputChangedEvent with a payload of false. The FocusOnKeyboardInputToggle method in the SearchUserControl class receives the payload for the event, and changes the FocusOnKeyboardInput property to false which disables type to search. When the SignInFlyout closes, the control is unloaded and the FocusOnKeyboardInputChangedEvent is published with a payload of true, to restore type to search functionality.
For more info about event aggregation see Communicating between loosely coupled components. For more info about type to search see Guidelines for search.
[Top]