Integrating Search with OData Service for Team Foundation Server in a Windows Store App

 

In this blog post, I will be discussing how to communicate with OData Service For TFS using Search contracts in a Windows Store App. The source code for my sample App is available for download as “TFSDashboardBeta.exe”.

In this sample App, I can search for a WorkItem across all the projects in TFS by entering the corresponding search term in the search box.

For example, if I enter “bug” in the search box, it will query the OData Service to look for WorkItems across all the projects that have the term “bug” matching with the State or Type property of a WorkItem.

This is what the search results look like when I search for the term “bug” against my project collection in TFS:

image

Clicking on a project title RadioButton will filter the search results to only show the WorkItems under that project. You can also see the count of search results under each of the projects represented by a number next to the project name.

Now I’ll go through the steps to achieve the above search results page.

Step 1: To add the Search Contract, right click on your project and go to “Add->New Item”. You will see the following dialog:

image

Look for “Search Contract” and rename it to “SearchResultsPage.xaml” and hit the ‘Add’ button. You will notice that two files are added to your project:

image

 

Step 2: The files that were added have the default search results DataTemplate. I want my search results to show names of the WorkItems and the creator of that WorkItem.

image

To achieve this, I’ll make changes to the SearchResultsPage.xaml page. Remove the predefined ItemTemplates from

GridView : 

ItemTemplate="{StaticResource StandardSmallIcon300x70ItemTemplate}"

and ListView:

ItemTemplate="{StaticResource StandardSmallIcon70ItemTemplate}"

 

And add the templates for the GridView and the ListView:

XAML

<GridView.ItemTemplate>
                        < DataTemplate>
                            <Grid HorizontalAlignment="Left" Width="294" Height="70" Background="#FF69B3E6">
                                < StackPanel Orientation="Vertical">
                                    < TextBlock Style="{StaticResource SearchTitleTextStyle}" Text="{Binding Title}" ToolTipService.ToolTip="{Binding Title}"/>
                                    < TextBlock Style="{StaticResource SearchDescriptionTextStyle}" Visibility="{Binding CreatedBy, Converter={StaticResource NullOrEmptyToVisibilityConverter}}">
                        <Run Text ="Created by: "></Run>
                        <Run Text="{Binding CreatedBy}"></Run>
                                    </TextBlock>
                                </StackPanel>
                            </Grid>
                        </DataTemplate>
                    </GridView.ItemTemplate>

<ListView.ItemTemplate>
                        < DataTemplate>
                            <Grid HorizontalAlignment="Left" Width="200" Height="70" Margin="6" Background="#FF69B3E6">
                                < StackPanel Orientation="Vertical">
                                    < TextBlock Style="{StaticResource SearchTitleTextStyle}" Text="{Binding Title}" ToolTipService.ToolTip="{Binding Title}"/>
                                    < TextBlock Style="{StaticResource SearchDescriptionTextStyle}" Visibility="{Binding CreatedBy, Converter={StaticResource NullOrEmptyToVisibilityConverter}}">
                        <Run Text ="Created by: "></Run>
                        <Run Text="{Binding CreatedBy}"></Run>
                                    </TextBlock>
                                </StackPanel>
                            </Grid>
                        </DataTemplate>
                    </ListView.ItemTemplate>

I have defined the styles used in the above code in the <Page.Resources> section of the SearchResultsPage.xaml:

<Style x:Key="SearchTitleTextStyle" TargetType="TextBlock" BasedOn="{StaticResource ItemGridTitleTextStyle}">
            <Setter Property="Margin" Value="6,5,0,0"></Setter>
            <Setter Property="FontSize" Value="18"></Setter>
        </Style>
        <Style x:Key="SearchDescriptionTextStyle" TargetType="TextBlock" BasedOn="{StaticResource ItemGridDescriptionTextStyle}">
            <Setter Property="Margin" Value="6,-5,0,0"></Setter>
            <Setter Property="FontSize" Value="14.5"></Setter>
        </Style>

Your styles and templates might differ depending upon how you want the search result items to appear.

 

Step 3: Rename the “App Name” in the string on the SearchResultsPage.xaml:

<x:String x:Key="AppName">App Name</x:String>

In my sample  code, I have removed this line because it is defined already in the App.xaml file as

<x:String x:Key="AppName">TFS Dashboard</x:String>

 

Step 4: Now, I have my search results’ template ready. Next, I want to work on retrieving the search results from the OData Service for TFS.

If my OData Service endpoint looks like this:

https://localhost:10042/DefaultCollection/ 

then I can search for the term “bug" in Type and State by constructing a query such as:

https://localhost:10042/DefaultCollection/WorkItems?$filter=substringof('bug',%20Type)%20eq%20true or substringof('bug',%20State)%20eq%20true

To achieve this, update the SearchResultsPage.xaml.cs file and replace the contents of the LoadState method to query the OData Service for results:

C#

var queryText = navigationParameter as String;
            var filterList = new List<Filter>();

if (queryText != string.Empty)
            {
               
                filterList.Add(new Filter("All", 0, true));

                string query = queryText.ToLower();
               
                String urlToQuery = "/WorkItems?$filter=substringof('"
                                + query + "',%20Type)%20eq%20true or substringof('" + query + "',%20State)%20eq%20true";

  //get the OData feeds for the search query

                await SampleDataSource.GetFeed(AuthorizationHelper.authorizationHeader, AuthorizationHelper.endpoint, urlToQuery);
                String description = "Search results for user entered query";
                Query queryItem = new Query("SearchQuery", description, DateTime.Now, description, description);     
               //parse the OData feeds to create WorkItem objects

                await SampleDataSource.GetWorkitems(queryItem);

                var all = new List<WorkItem>();
                all = queryItem.Items.ToList();
                _results.Add("All", all);

                for (int i = 0; i < SampleDataSource.Projects.Count; i++)
                {
                    var matches = queryItem.Items.Where(item => item.Project.Equals(SampleDataSource.Projects[i]));
                    if (matches.Count() > 0)
                    {
                        filterList.Add(new Filter(SampleDataSource.Projects[i], matches.Count(), false));
                        _results.Add(SampleDataSource.Projects[i], matches.ToList());
                    }
                }

                filterList[0].Count = all.Count;

                // Communicate results through the view model
                this.DefaultViewModel["ResultText"] = "Results for " + '\u201c' + queryText + '\u201d';
            }
            else
            {
                // Communicate results through the view model
                this.DefaultViewModel["ResultText"] = "Enter a search term";
            }

            this.DefaultViewModel["Filters"] = filterList;
            this.DefaultViewModel["ShowFilters"] = filterList.Count > 1;

 

The “for loop” above goes through all the projects the WorkItems belong to and then binds filterList to the RadioButton for project selection and _results to GridView/ListView for displaying the results.

Include this list of results in the class SearchResultsPage.xaml.cs, before the constructor:

C#

private Dictionary<string, List<WorkItem>> _results = new Dictionary<string, List<WorkItem>>();

 

The GetFeed method called in the LoadState method above and defined in the SampleDataSource.cs file retrieves the OData feed for the search query:

C#

public static async Task GetFeed(string authHeader, string endpoint, string varQuery)
        {
            SyndicationClient client = new SyndicationClient();
            client.BypassCacheOnRetrieve = true;
            client.SetRequestHeader("Authorization", authHeader);
            Uri uri = null;

            try
            {
                if (varQuery != null)
                {
                    uri = new Uri(string.Format(CultureInfo.InvariantCulture, "{0}/{1}", endpoint, varQuery));
                }
                else
                {
                    uri = new Uri(endpoint);
                }

                currentFeed = await client.RetrieveFeedAsync(uri);
            }
            catch (Exception ex)
            {
                //assign the query that failed
                failedQuery = uri.ToString();
                throw ex;
            }
        }

For more information on the specific functions and properties mentioned in the above code, please download the source code.

 

Step 5: If the user selects a different project by clicking on its RadioButton, we need to appropriately update the UI to reflect the search results for the selected project. To achieve this, go to Filter_SelectionChanged method in the SearchResultsPage.xaml.cs page and add this line of code, right after “//TODO” statement:

C#

this.DefaultViewModel["Results"] = _results[selectedFilter.Name];

 

Step 6: We can also add hint text in the App search box to guide the user. For instance, in the below screenshot, “Search on State or WorkItemType” act as a hint text.

image

To enable the hint text, we can set the PlaceholderText for the SearchPane in the OnLaunched() event in App.xaml.cs:

C#

SearchPane.GetForCurrentView().PlaceholderText = "Search on State/WorkItemType";

 

In this sample App, I have also added support for external launching of the App via the Search Pane. You can find more information on it in the hands on labs and my source code that is available for download.

For more information on Search contracts, I have recorded a video on Channel9.