Created
August 21, 2012 20:44
-
-
Save markdstafford/3419288 to your computer and use it in GitHub Desktop.
Sample Windows Store app with OData consumption
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Collections.ObjectModel; | |
using System.Collections.Specialized; | |
using System.Data.Services.Client; | |
using System.Linq; | |
using System.Text; | |
using System.Text.RegularExpressions; | |
using System.Threading.Tasks; | |
using OData.WindowsStore.NetflixDemo.Common; | |
using OData.WindowsStore.NetflixDemo.Netflix; | |
using Windows.Foundation.Metadata; | |
using Windows.UI.Xaml.Media; | |
using Windows.UI.Xaml.Media.Imaging; | |
namespace OData.WindowsStore.NetflixDemo.Data | |
{ | |
public class SampleDataSource | |
{ | |
private static readonly SampleDataSource Instance = new SampleDataSource(); | |
private static readonly NetflixCatalog Context = new NetflixCatalog(new Uri("http://odata.netflix.com/Catalog")); | |
private readonly ObservableCollection<SampleDataGroup> allGroups = new ObservableCollection<SampleDataGroup>(); | |
static SampleDataSource() | |
{ | |
LoadMovies(); | |
} | |
public ObservableCollection<SampleDataGroup> AllGroups | |
{ | |
get { return allGroups; } | |
} | |
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()); | |
} | |
public static IEnumerable<SampleDataGroup> GetGroups(string uniqueId) | |
{ | |
if (!uniqueId.Equals("AllGroups")) | |
throw new ArgumentException("Only 'AllGroups' is supported as a collection of groups"); | |
return Instance.AllGroups; | |
} | |
public static SampleDataGroup GetGroup(string id) | |
{ | |
IEnumerable<SampleDataGroup> matches = | |
Instance.AllGroups.Where(group => group.UniqueId.Equals(id)); | |
return matches.FirstOrDefault(); | |
} | |
public static SampleDataItem GetItem(string id) | |
{ | |
IEnumerable<SampleDataItem> matches = | |
Instance.AllGroups.SelectMany(group => group.Items).Where(item => item.UniqueId.Equals(id)); | |
return matches.FirstOrDefault(); | |
} | |
/// <summary> | |
/// Loads movies asynchronously onto the singleton. | |
/// </summary> | |
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())); | |
} | |
} | |
} | |
} | |
[WebHostHidden] | |
public abstract class ODataBindable : BindableBase | |
{ | |
private static readonly Uri BaseUri = new Uri("ms-appx:///"); | |
private string description = string.Empty; | |
private ImageSource image; | |
private String imagePath; | |
private string subtitle = string.Empty; | |
private string title = string.Empty; | |
private string uniqueId = string.Empty; | |
protected ODataBindable(String uniqueId, String title, String subtitle, String imagePath, String description) | |
{ | |
this.uniqueId = uniqueId; | |
this.title = title; | |
this.subtitle = subtitle; | |
this.description = description; | |
this.imagePath = imagePath; | |
} | |
public string UniqueId | |
{ | |
get { return uniqueId; } | |
set { SetProperty(ref uniqueId, value); } | |
} | |
public string Title | |
{ | |
get { return title; } | |
set { SetProperty(ref title, value); } | |
} | |
public string Subtitle | |
{ | |
get { return subtitle; } | |
set { SetProperty(ref subtitle, value); } | |
} | |
public string Description | |
{ | |
get { return description; } | |
set { SetProperty(ref description, value); } | |
} | |
public ImageSource Image | |
{ | |
get | |
{ | |
if (image == null && imagePath != null) | |
{ | |
image = new BitmapImage(new Uri(BaseUri, imagePath)); | |
} | |
return image; | |
} | |
set | |
{ | |
imagePath = null; | |
SetProperty(ref image, value); | |
} | |
} | |
public void SetImage(String path) | |
{ | |
image = null; | |
imagePath = path; | |
OnPropertyChanged("Image"); | |
} | |
public override string ToString() | |
{ | |
return Title; | |
} | |
} | |
/// <summary> | |
/// Functionally represents a genre in the Netflix catalog. Name and functionality | |
/// left as-is to minimize changes to XAML bindings. | |
/// </summary> | |
public class SampleDataGroup : ODataBindable | |
{ | |
private readonly ObservableCollection<SampleDataItem> items = new ObservableCollection<SampleDataItem>(); | |
private readonly ObservableCollection<SampleDataItem> topItem = new ObservableCollection<SampleDataItem>(); | |
public SampleDataGroup(String uniqueId, String title, String subtitle, String imagePath, String description) | |
: base(uniqueId, title, subtitle, imagePath, description) | |
{ | |
Items.CollectionChanged += ItemsCollectionChanged; | |
} | |
public ObservableCollection<SampleDataItem> Items | |
{ | |
get { return items; } | |
} | |
public ObservableCollection<SampleDataItem> TopItems | |
{ | |
get { return topItem; } | |
} | |
private void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) | |
{ | |
// Provides a subset of the full items collection to bind to from a GroupedItemsPage | |
// for two reasons: GridView will not virtualize large items collections, and it | |
// improves the user experience when browsing through groups with large numbers of | |
// items. | |
// | |
// A maximum of 12 items are displayed because it results in filled grid columns | |
// whether there are 1, 2, 3, 4, or 6 rows displayed | |
switch (e.Action) | |
{ | |
case NotifyCollectionChangedAction.Add: | |
foreach (SampleDataItem movie in e.NewItems) | |
{ | |
movie.Group = this; | |
} | |
if (e.NewStartingIndex < 12) | |
{ | |
TopItems.Insert(e.NewStartingIndex, Items[e.NewStartingIndex]); | |
if (TopItems.Count > 12) | |
{ | |
TopItems.RemoveAt(12); | |
} | |
} | |
break; | |
case NotifyCollectionChangedAction.Move: | |
if (e.OldStartingIndex < 12 && e.NewStartingIndex < 12) | |
{ | |
TopItems.Move(e.OldStartingIndex, e.NewStartingIndex); | |
} | |
else if (e.OldStartingIndex < 12) | |
{ | |
TopItems.RemoveAt(e.OldStartingIndex); | |
TopItems.Add(Items[11]); | |
} | |
else if (e.NewStartingIndex < 12) | |
{ | |
TopItems.Insert(e.NewStartingIndex, Items[e.NewStartingIndex]); | |
TopItems.RemoveAt(12); | |
} | |
break; | |
case NotifyCollectionChangedAction.Remove: | |
if (e.OldStartingIndex < 12) | |
{ | |
TopItems.RemoveAt(e.OldStartingIndex); | |
if (Items.Count >= 12) | |
{ | |
TopItems.Add(Items[11]); | |
} | |
} | |
break; | |
case NotifyCollectionChangedAction.Replace: | |
if (e.OldStartingIndex < 12) | |
{ | |
TopItems[e.OldStartingIndex] = Items[e.OldStartingIndex]; | |
} | |
break; | |
case NotifyCollectionChangedAction.Reset: | |
TopItems.Clear(); | |
while (TopItems.Count < Items.Count && TopItems.Count < 12) | |
{ | |
TopItems.Add(Items[TopItems.Count]); | |
} | |
break; | |
} | |
} | |
} | |
/// <summary> | |
/// Functionally represents a movie in the Netflix catalog. Name and functionality | |
/// left as-is to minimize changes to XAML bindings. | |
/// </summary> | |
public class SampleDataItem : ODataBindable | |
{ | |
private string content = string.Empty; | |
private SampleDataGroup group; | |
public SampleDataItem(String uniqueId, String title, String subtitle, String imagePath, String description, | |
String content) | |
: base(uniqueId, title, subtitle, imagePath, description) | |
{ | |
this.content = content; | |
} | |
public string Content | |
{ | |
get { return content; } | |
set { SetProperty(ref content, value); } | |
} | |
public SampleDataGroup Group | |
{ | |
get { return group; } | |
set { SetProperty(ref group, value); } | |
} | |
} | |
/// <summary> | |
/// Allows us to get a distinct set of search results. | |
/// </summary> | |
public class SampleDataItemComparer : IEqualityComparer<SampleDataItem> | |
{ | |
#region IEqualityComparer<SampleDataItem> Members | |
public bool Equals(SampleDataItem x, SampleDataItem y) | |
{ | |
return String.Equals(x.UniqueId, y.UniqueId, StringComparison.Ordinal); | |
} | |
public int GetHashCode(SampleDataItem obj) | |
{ | |
return obj.UniqueId.GetHashCode(); | |
} | |
#endregion | |
} | |
public static class ExtensionMethods | |
{ | |
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); | |
} | |
public static async Task<IEnumerable<TResult>> ExecuteAsync<TResult>(this DataServiceContext context, | |
Uri requestUri) | |
{ | |
return await Task.Factory.FromAsync<IEnumerable<TResult>>(context.BeginExecute<TResult>(requestUri, null, null), | |
executeAsyncResult => | |
{ | |
List<TResult> executeResult = | |
context.EndExecute<TResult>(executeAsyncResult) | |
.ToList(); | |
return executeResult; | |
}); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.Linq; | |
using OData.WindowsStore.NetflixDemo.Data; | |
using Windows.ApplicationModel.Activation; | |
using Windows.Foundation; | |
using Windows.Foundation.Collections; | |
using Windows.UI.Xaml; | |
using Windows.UI.Xaml.Controls; | |
using Windows.UI.Xaml.Controls.Primitives; | |
using Windows.UI.Xaml.Data; | |
using Windows.UI.Xaml.Input; | |
using Windows.UI.Xaml.Media; | |
using Windows.UI.Xaml.Navigation; | |
// The Search Contract item template is documented at http://go.microsoft.com/fwlink/?LinkId=234240 | |
namespace OData.WindowsStore.NetflixDemo | |
{ | |
/// <summary> | |
/// This page displays search results when a global search is directed to this application. | |
/// </summary> | |
public sealed partial class SearchResultsPage : OData.WindowsStore.NetflixDemo.Common.LayoutAwarePage | |
{ | |
public SearchResultsPage() | |
{ | |
this.InitializeComponent(); | |
} | |
/// <summary> | |
/// Populates the page with content passed during navigation. Any saved state is also | |
/// provided when recreating a page from a prior session. | |
/// </summary> | |
/// <param name="navigationParameter">The parameter value passed to | |
/// <see cref="Frame.Navigate(Type, Object)"/> when this page was initially requested. | |
/// </param> | |
/// <param name="pageState">A dictionary of state preserved by this page during an earlier | |
/// session. This will be null the first time a page is visited.</param> | |
protected override void LoadState(Object navigationParameter, Dictionary<String, Object> pageState) | |
{ | |
var queryText = navigationParameter as String; | |
// TODO: Application-specific searching logic. The search process is responsible for | |
// creating a list of user-selectable result categories: | |
// | |
// filterList.Add(new Filter("<filter name>", <result count>)); | |
// | |
// Only the first filter, typically "All", should pass true as a third argument in | |
// order to start in an active state. Results for the active filter are provided | |
// in Filter_SelectionChanged below. | |
var filterList = new List<Filter>(); | |
filterList.Add(new Filter("All", 0, true)); | |
// Communicate results through the view model | |
this.DefaultViewModel["QueryText"] = queryText; | |
this.DefaultViewModel["Filters"] = filterList; | |
this.DefaultViewModel["ShowFilters"] = filterList.Count > 1; | |
} | |
/// <summary> | |
/// Invoked when a filter is selected using the ComboBox in snapped view state. | |
/// </summary> | |
/// <param name="sender">The ComboBox instance.</param> | |
/// <param name="e">Event data describing how the selected filter was changed.</param> | |
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); | |
} | |
/// <summary> | |
/// Invoked when a filter is selected using a RadioButton when not snapped. | |
/// </summary> | |
/// <param name="sender">The selected RadioButton instance.</param> | |
/// <param name="e">Event data describing how the RadioButton was selected.</param> | |
void Filter_Checked(object sender, RoutedEventArgs e) | |
{ | |
// Mirror the change into the CollectionViewSource used by the corresponding ComboBox | |
// to ensure that the change is reflected when snapped | |
if (filtersViewSource.View != null) | |
{ | |
var filter = (sender as FrameworkElement).DataContext; | |
filtersViewSource.View.MoveCurrentTo(filter); | |
} | |
} | |
/// <summary> | |
/// View model describing one of the filters available for viewing search results. | |
/// </summary> | |
private sealed class Filter : OData.WindowsStore.NetflixDemo.Common.BindableBase | |
{ | |
private String _name; | |
private int _count; | |
private bool _active; | |
public Filter(String name, int count, bool active = false) | |
{ | |
this.Name = name; | |
this.Count = count; | |
this.Active = active; | |
} | |
public override String ToString() | |
{ | |
return Description; | |
} | |
public String Name | |
{ | |
get { return _name; } | |
set { if (this.SetProperty(ref _name, value)) this.OnPropertyChanged("Description"); } | |
} | |
public int Count | |
{ | |
get { return _count; } | |
set { if (this.SetProperty(ref _count, value)) this.OnPropertyChanged("Description"); } | |
} | |
public bool Active | |
{ | |
get { return _active; } | |
set { this.SetProperty(ref _active, value); } | |
} | |
public String Description | |
{ | |
get { return String.Format("{0} ({1})", _name, _count); } | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment