OData 101: Building our first OData-based Windows Store app (Part 2)
In the previous blog post, we walked through the steps to build an OData-enabled client using the new Windows UI. In this blog post, we’ll take a look at some of the code that makes it happen.
ODataBindable, SampleDataItem and SampleDataGroup
In the walkthrough, we repurposed SampleDataSource.cs with some code from this gist. In that gist, ODataBindable, SampleDataItem and SampleDataGroup were all stock classes from the project template (ODataBindable was renamed from SampleDataCommon, but otherwise the classes are exactly the same).
ExtensionMethods
The extension methods class contains two simple extension methods. Each of these extension methods uses the Task-based Asynchronous Pattern (TAP) to allow the SampleDataSource to execute an OData query without blocking the UI.
For instance, the following code uses the very handy Task.Factory.FromAsync method to implement TAP:
public static async Task<IEnumerable<T>> ExecuteAsync<T>(this DataServiceQuery<T> query) { return await Task.Factory.FromAsync<IEnumerable<T>>(query.BeginExecute(null, null), query.EndExecute); }
SampleDataSource
The SampleDataSource class has a significant amount of overlap with the stock implementation. The changes I made were to bring it just a bit closer to the Singleton pattern and the implementation of two important methods.
Search
The Search method is an extremely simplistic implementation of search. In this case it literally just does an in-memory search of the loaded movies. It is very easy to imagine passing the search term through to a .Where() clause, and I encourage you to do so in your own implementation. In this case I was trying to keep the code as simple as possible.
public static IEnumerable<SampleDataItem> Search(string searchString) { var regex = new Regex(searchString, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); return Instance.AllGroups .SelectMany(g => g.Items) .Where(m => regex.IsMatch(m.Title) || regex.IsMatch(m.Subtitle)) .Distinct(new SampleDataItemComparer()); }
LoadMovies
The LoadMovies method is where the more interesting code exists.
public static async void LoadMovies() { IEnumerable<Title> titles = await ((DataServiceQuery<Title>)Context.Titles .Expand("Genres,AudioFormats,AudioFormats/Language,Awards,Cast") .Where(t => t.Rating == "PG") .OrderByDescending(t => t.ReleaseYear) .Take(300)).ExecuteAsync(); foreach (Title title in titles) { foreach (Genre netflixGenre in title.Genres) { SampleDataGroup genre = GetGroup(netflixGenre.Name); if (genre == null) { genre = new SampleDataGroup(netflixGenre.Name, netflixGenre.Name, String.Empty, title.BoxArt.LargeUrl, String.Empty); Instance.AllGroups.Add(genre); } var content = new StringBuilder(); // Write additional things to content here if you want them to display in the item detail. genre.Items.Add(new SampleDataItem(title.Id, title.Name, String.Format("{0}\r\n\r\n{1} ({2})", title.Synopsis, title.Rating, title.ReleaseYear), title.BoxArt.HighDefinitionUrl ?? title.BoxArt.LargeUrl, "Description", content.ToString())); } } }
The first and most interesting thing we do is to use the TAP pattern again to asynchronously get 300 (Take) recent (OrderByDescending) PG-rated (Where) movies back from Netflix. The rest of the code is simply constructing SimpleDataItems and SimpleDataGroups from the entities that were returned in the OData feed.
SearchResultsPage
Finally, we have just a bit of calling code in SearchResultsPage. When a user searches from the Win+F experience, the LoadState method is called first, enabling us to intercept what was searched for. In our case, the stock implementation is okay aside from the fact that we don’t any additional quotes embedded, so we’ll modify the line that puts the value into the DefaultViewModel to not append quotes:
this.DefaultViewModel["QueryText"] = queryText;
When the filter actually changes, we want to pass the call through to our implementation of search, which we can do with the stock implementation of Filter_SelectionChanged:
void Filter_SelectionChanged(object sender, SelectionChangedEventArgs e) { // Determine what filter was selected var selectedFilter = e.AddedItems.FirstOrDefault() as Filter; if (selectedFilter != null) { // Mirror the results into the corresponding Filter object to allow the // RadioButton representation used when not snapped to reflect the change selectedFilter.Active = true; // TODO: Respond to the change in active filter by setting this.DefaultViewModel["Results"] // to a collection of items with bindable Image, Title, Subtitle, and Description properties var searchValue = (string)this.DefaultViewModel["QueryText"]; this.DefaultViewModel["Results"] = new List<SampleDataItem>(SampleDataSource.Search(searchValue)); // Ensure results are found object results; ICollection resultsCollection; if (this.DefaultViewModel.TryGetValue("Results", out results) && (resultsCollection = results as ICollection) != null && resultsCollection.Count != 0) { VisualStateManager.GoToState(this, "ResultsFound", true); return; } } // Display informational text when there are no search results. VisualStateManager.GoToState(this, "NoResultsFound", true); }
Item_Clicked
Optionally, you can implement an event handler that will cause the page to navigate to the selected item by copying similar code from GroupedItemsPage.xaml.cs. The event binding will also need to be added to the resultsGridView in XAML. You can see this code in the published sample.