I have created a video of the application.
Contents
Introduction
This article is the second, and final, part of the Netflix Browser for Windows Phone 7 article. Part 1 focused on exploring the Pivot and Panorama controls in general, and demonstrated the first steps of the demo application walkthrough. We learned how to add the controls to a project, and how to work with the Pivot and Panorama templates. In this part, we will continue with the walk-through of the demo application, and in particular we will look at OData and how to consume OData in a Windows Phone application. We will also explore the Silverlight Toolkit’s WrapPanel
control, page navigation and the progress bar.
Demo Application Overview
The demo application is a Panorama application that allows users to browse through Netflix data. At the top layer the user is presented with a list of genres, new releases and highest rated movies. The user can then drill down into each movie to get movie details. Selecting a genre from the genre list will take the user to a pivot which lists movies for the selected genre representing the data in three different ways: all movies for that genre, sorted by year, and sorted by average rating. In an ideal situation, I would like to see the list of all genres to be only the top level genres, instead of a very long list of genres and subgenres; but there wasn’t an easy way to query for this data.
What Will be Covered
- Panorama Control (Part 1)
- Pivot Control (Part 1)
- Consuming OData
- WrapPanel
- Page Navigation
- Saving and Restoring Transient State
- Progress Bar
Prerequisites
- Windows Vista or Windows 7
- Windows Phone 7 Developers Tools which includes Visual Studio Express 2010 for Windows Phone, Windows Phone Emulator, Expression Blend 4 for Windows Phone, and XNA Game Studio 4.0.
- Visual Studio Express 2010 and Expression Bled 4 will only be installed if you do not have them already installed.
- If you have previously installed the Beta Tools, you will have to uninstall it before installing the final tools. Note that the installation may take some time; mine took well over an hour. Leave the installation running, it will eventually complete.
- Silverlight for Windows Phone Toolkit
- OData Client Library for Windows Phone 7 (note that this is a production-ready version announced at PDC 2010; in Part 1 we used a preview version)
- There are three downloads on the CodePlex site. For the purpose of this demo, all we need is the ODataClient_BinariesAndCodeGenToolForWinPhone.zip. It contains the OData client assemblies and Datasvcutil code generation tool which will be used for generating the proxy classes.
OData
The demo application consumes Netflix data which is exposed using the Open Data Protocol (OData). There is a heap of information on OData on odata.org, but I liked Shayne Burgess’s explanation of what OData is, so I pasted it below.
In very simple terms, OData is a resource-based Web protocol for querying and updating data. OData defines operations on resources using HTTP verbs (PUT, POST, UPDATE and DELETE), and it identifies those resources using a standard URI syntax. Data is transferred over HTTP using the AtomPub or JSON standards. For AtomPub, the OData protocol defines some conventions on the standard to support the exchange of query and schema information. (Source: MSDN Magazine, June 2010 Issue)
The OData protocol enables access to information from a broad range of clients. At present, there are client libraries available for Windows Phone 7, iPhone, Silverlight 4, PHP, AJAX/Javascript, Ruby, or Java. Here you can see a complete list. In the demo, we use the OData Client Library for Windows Phone 7. An OData service can be implemented on any server that supports HTTP. The .NET implementation is supported through WCF Data Services. WCF Data Services is a .NET Framework component which used to be known as ADO.Net Data Services (codename Astoria). The WCF Data Services provides a framework for creating OData Web services and includes a set of client libraries (one for general .NET framework client applications and one for Silverlight applications) for building clients that consume OData feeds.
Services that expose their data using the OData protocol are referred to as OData producers, and clients that consume data exposed using the OData protocol are referred to as consumers. The odata.org website provides a list of current producers and consumers. Among the producers, there are applications such as SharePoint 2010, Windows Azure Storage, or IBM WebSphere, as well as several live OData services such as the Netflix Data service used in the demo application. The consumers list for example includes Excel 2010. Microsoft released the OData specification under Open Specification Promise (OSP) which means that anyone can freely build OData clients and services. There are resources available on how to create an OData service, for example you can check out Scott Hanselman’s post on creating a custom API for StackOverflow. For an example on how to consume OData in a Windows Phone application, see the next section.
Consuming OData in a WP7 application
In a Silverlight project and Visual Studio 2010, all you need to do to start consuming OData is to Add Service Reference and specify the OData service URL as shown in Figure 1. Visual Studio then automatically adds a reference to the System.Data.Services.Client.dll assembly, creates a client-side context class which is used to interact with the data service, and generates a set of proxy classes that represent the types exposed by the service.
Figure 1: Add Service Reference
The Add Service Reference for OData is missing for Windows Phone applications, but as you will see, you can get up and running with WCF Data Services in your Windows Phone application in a few simple steps:
- Add a reference to the System.Data.Services.Client.dll assembly from the OData Client Library for Windows Phone 7 (see Prerequisites for the download link).
- In Solution Explorer in Visual Studio, right click on References and select Add Reference. Browse to the location where you saved the OData Client Library for Windows Phone 7 and select the System.Data.Services.Client.dll assembly.
- Run the Datasvcutil tool to generate the data service class.
- The generated class is comprised of a context class called
NetflixCatalog
, and client proxy classes for each type exposed by the Netflix service. Because we used the /DataServiceCollection option in the command above, the generated proxy classes implement the INotifyPropertyChanged
interface.
- Copy the generated class to your project and include it in your project.
- In the demo, I created a folder called Services where I copied the generated class.
- Select the Show All button in the Solution Explorer in Visual Studio, right click on the newly generated class and select the Include In Project option.
Working with OData
Once we have our project setup, we can begin to interact with the Netflix OData service. In the demo, we do this in the MainViewModel
class.
As a first step, we create the context that will allow us to interact with the Netflix Catalog.
using alias=NetflixCatalog.Model;
.
.
.
readonly alias.NetflixCatalog context;
public MainViewModel()
{
context = new alias.NetflixCatalog(new Uri("http://odata.netflix.com/Catalog/",
UriKind.Absolute));
}
You may be surprised that I used an alias in the above excerpt. I chose this as a workaround for a name conflict. You see, unfortunately when the new DataSvcUtil tool generates the Netflix service class, it assigns the namespace NetflixCatalog.Model, which conflicts with the name NetflixCatalog that the tool assigns to the context class.
Then we go on to querying the service. Recall that the Panorama has three sections: one for displaying a list of all genres, one for new titles, and the last for top titles. For this, I have created three methods: LoadGenres
, LoadNewTitles
, and LoadTopTitles
, where I formulate a query to retrieve the desired Netflix data, and load the query result to a collection.
Before we take a look at these methods, let's first see how the collection members are declared. As you can see we use DataServiceCollection
. The DataServiceCollection
is an ObservableCollection
and makes working with WCF Data Services very easy.
DataServiceCollection<Genre> genres;
DataServiceCollection<Title> newTitles;
DataServiceCollection<Title> topTitles;
Both the Netflix API and the OData client library for Windows Phone 7, used in Part 1 of this article, were officially in a preview mode. The Netflix API remains in a preview mode; however, an updated, production-ready version of the OData Client Library for Windows Phone 7 has been released. One of the main changes with the update is that the new version doesn’t provide support for LINQ. The preview version gave us some LINQ support, and the demo application in Part 1 used LINQ to formulate queries. This would no longer work with the new library. Therefore, we need to replace the LINQ queries with URI based queries. Note though that the LINQ support may be enabled in future releases. Originally, the LoagGenres
method looked like this:
public void LoadGenres()
{
genres = new DataServiceCollection<genre>();
IQueryable<genre> query = from g in context.Genres
select g;
genres.LoadAsync(query);
genres.LoadCompleted += (sender, args) =>
{
if (args.Error != null)
{
Debug.WriteLine("Requesting titles failed. "
+ args.Error.Message);
}
else
{
IsDataLoaded = true;
}
};
}
As you saw above, we began by instantiating the genres DataServiceCollection
. Then we formulated a LINQ query (this is the part we will replace). The DataServiceCollection
has a LoadAsync
method which handles the asynchronous callback necessary for network calls in Silverlight, and loads query result into the collection. We used the LoadAsync
method to load the query result into our genres collection. The method ended with an event handler that handles the LoadCompleted
event of the asynchronous callback.
Now we need to replace the LINQ query with an URI query. To get an idea on how to formulate the URI query, we can check out the sample queries and guidance on the Netflix website. Alternatively since we still have our LINQ query, and since the LINQ query gets interpreted into the URI, we can simply use Add Watch while debugging in Visual Studio and extract the query value. We could also use a tool like Fiddler to extract the URI, as it's being sent to the Netflix service.
For the list of genres in the LoadGenres
method, the URI query is very simple:
http://odata.netflix.com/Catalog/Genres()
We can then replace the LINQ query with this:
Uri uriQuery = new Uri("/Genres()", UriKind.Relative);
Note that we didn’t need to specify all parts of the URI. This is because the first part (http://odata.netflix.com/Catalog) of the URI has already been defined as the DataServiceContext
(see the beginning of this section) which as you can see below, we pass as a parameter when we instantiate the genres DataServiceCollection
.
The WCF Data Services team has added a LoadAsync(Uri)
method to the DataServiceCollection
class which makes our life very easy, and we can just replace the parameter in the LoadAsync
method with the URI query. The amended method looks like this:
public void LoadGenres()
{
genres = new DataServiceCollection<genre>(context);
Uri uriQuery = new Uri("/Genres()", UriKind.Relative);
genres.LoadAsync(uriQuery);
genres.LoadCompleted += (sender, args) =>
{
if (args.Error != null)
{
Debug.WriteLine("Requesting titles failed. " + args.Error.Message);
}
else
{
IsDataLoaded = true;
}
};
}
We want to bind the result of the queries to the Panorama control. So in the MainViewModel
, we declare properties for the genres, newTitles and topTitles DataServiceCollection
s:
public DataServiceCollection<Genre> Genres
{
get
{
return genres;
}
private set
{
genres = value;
OnPropertyChanged("Genres");
}
}
public DataServiceCollection<Title> TopTitles
{
get
{
return topTitles;
}
private set
{
topTitles = value;
OnPropertyChanged("TopTitles");
}
}
public DataServiceCollection<Title> NewTitles
{
get
{
return newTitles;
}
private set
{
newTitles = value;
OnPropertyChanged("NewTitles");
}
}
In the MainPage
, we set the ItemSource
property of each ListBox
control in the Panorama control to bind to the corresponding property shown above. Also, we bind the Text
property of the TextoBlock
control to the appropriate field name. Below is an example of the first PanoramaItem
, where we show a list of all genres.
<!---->
<controls:PanoramaItem Header="genres" >
<ListBox x:Name="GenreListBox" Margin="0,0,-12,0"
ItemsSource="{Binding Genres}"
SelectionChanged="GenreListBoxSelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Margin="0,0,0,17" Width="432">
<TextBlock Text="{Binding Name}" TextWrapping="Wrap"
Foreground="{StaticResource PanoramaForegroundBrush}"
Style="{StaticResource PhoneTextLargeStyle}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</controls:PanoramaItem>
To complete the data binding, we set the DataContext
of the Panorama page (MainPage.cs) to the MainViewModel
:
public MainPage()
{
InitializeComponent();
DataContext = new MainViewModel();
}
MainViewModel ViewModel
{
get
{
return (MainViewModel)DataContext;
}
}
WrapPanel
The third panorama item shows top 20 DVDs as thumb images. I decided to utilize the Silverlight for Windows Phone Toolkit, and chose to lay out the images with a WrapPanel
. As the name suggests a WrapPanel
wraps content inside its boundaries. So you can define the orientation (horizontal by default), width and height and the content will be wrapped nicely inside it (see Figure 2). This is opposite to the StackPanel
, which stacks content either horizontally or vertically and doesn’t wrap its content.
Figure 2: WrapPanel layout
To use the toolkit, we need to add a reference to the Microsoft.Phone.Controls.Toolkit.dll assembly, and a namespace reference to MainPage.xaml.
xmlns:toolkit="clr-namespace:Microsoft.Phone.Controls;
assembly=Microsoft.Phone.Controls.Toolkit"
Because the content of this Panorama section is wider than the screen, we set the PanoramaItem.Orientation
property to Horizontal
. As you can see in the excerpt below we use a ListBox
control with an ItemsPanelTemplate
where we specify the Panel
for laying out the items to be the WrapPanel
. We set the width of the WrapPanel
to 600px. The orientation is horizontal by default. We then set the ListBox
DataTemplate
to the image and bind its source to the appropriate field:
<!---->
<controls:PanoramaItem Header="top rated"
Orientation="Horizontal">
<ListBox Margin="0,0,-12,0" x:Name="TopTitlesListBox"
ItemsSource="{Binding TopTitles}"
SelectionChanged="TitlesListBoxSelectionChanged">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<toolkit:WrapPanel x:Name="wrapPanel" Width="600" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Image Source="{Binding BoxArt.MediumUrl}" Width="100" Margin="10"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</controls:PanoramaItem>
Page Navigation
Page navigation in Windows Phone Silverlight applications is based on the Silverlight 3 Frame and Page navigation model. However, Windows Phone applications are built using the PhoneApplicationFrame
and PhoneApplicationPage
instead of the Silverlight’s 3 Frame
and Page
controls. This is an important point, and you must not use the standard Silverlight Frame
and Page
types. The PhoneApplicationFrame
and PhoneApplicationPage
controls are derived from the Frame
and Page
classes respectively. The PhoneApplicationFrame
serves as a container for PhoneApplicaitonPage
controls, and contains other elements such as the system tray and application bar. The PhoneApplicaitonPage
holds sections of the application’s content. There can be only one PhoneApplicationFrame
for an application. The idea is that the single frame container can facilitate the navigating (switching) between pages. You can create as many pages as you like, but you must be aware that each page is retained on the navigation stack, and too many pages in your application, will increase the size of the navigation stack and can degrade performance.
Navigating between pages is similar to browsing the web with a web browser. Backward navigation occurs either through the Windows Phone Back hardware button, which takes the user either to the previous page or exits the application, or via the NavigationService
.
Navigating to GenreDvds Page
After selecting a genre from the genre list shown in the first Panorama section, the user is navigated to a GenreDvds page and shown the genre’s titles. The user can then return to the previous page via the hardware back button (see Figure 3).
Figure 3: Navigating to GenreDvds Page
When the user selects a genre, the ListBox’s SelectionChanged
event is triggered. In the handler, we retrieve the selected genre and save it as a Genre
object. Then we use a DataLayer
class to save the genre’s titles and use the NavigationService
’s Navigate
method to navigate to the GenreDvds page. The Navigate
method takes a URI as a parameter, which we specify as the path to the GenreDvds page, relative to the projects root directory and pass it a string
parameter genreId
. Because the gerneId
is a genre name, we use the HttpUtility.UrlEncode
method to encode it into a string that is URL friendly. At the end of the method, we reset the selected item.
void GenreListBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var selectedGenre = GenreListBox.SelectedItem as Genre;
if (selectedGenre != null)
{
string genreId = HttpUtility.UrlEncode(selectedGenre.Name);
DataLayer.SetTitlesForGenre(genreId, selectedGenre.Titles);
NavigationService.Navigate(new Uri("/GenreDvds.xaml?name=" +
genreId, UriKind.Relative));
}
GenreListBox.SelectedItem = null;
}
On the GenreDvds
page, we retrieve the string
parameter and call the viewmodel
’s LoadGenreTitles
method passing it the retrieved genreId
. We do this in the OnNavigatedTo
method which gets called when the page comes into view.
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
string genreId;
if (!NavigationContext.QueryString.TryGetValue("name", out genreId))
{
MessageBox.Show("No genre provided.");
return;
}
viewModel.LoadGenreTitles(genreId);
}
The DataContext
of the GenreDvds
page is set to the GenreDvdsViewModel
in the constructor.
readonly GenreDvdsViewModel viewModel = new GenreDvdsViewModel();
public GenreDvds()
{
InitializeComponent();
DataContext = viewModel;
}
In the LoadGenreTitles
method, we use the DataLayer
class to retrieve the titles of the selected genre and set the SelectedGenreName
property to the genre name. We also set the Loaded
and Busy
properties which we use to set the visibility of the ProgressBar
control on the GenreDvds
page. This will be explained in the section entitled Progress Bar. Then we asynchronously load the related genre titles. The genreTitles
field is a DataServiceCollection
, which is declared as a property called GenreTitles
used in data binding. The LoadGenreTitles
method is in the GenreDvdsViewModel
.
public void LoadGenreTitles(string genreName)
{
string genreId = HttpUtility.UrlEncode(genreName);
genreTitles = DataLayer.GetTitlesForGenre(genreId);
SelectedGenreName = genreName;
Loaded = false;
Busy = true;
if (genreTitles.Count() == 0)
{
genreTitles.LoadAsync();
}
else
{
Loaded = true;
Busy = false;
GetTitlesByYear(genreTitles);
GetTitlesByRating(genreTitles);
}
genreTitles.LoadCompleted += (sender, args) =>
{
if (args.Error != null)
{
Debug.WriteLine(
"Requesting titles failed. "
+ args.Error.Message);
}
Loaded = true;
Busy = false;
GetTitlesByYear(genreTitles);
GetTitlesByRating(genreTitles);
};
}
Navigating to Details Page
The navigation to the details page begins when the user selects an item in the second or third Panorama section and is dealt with in the same way as explained above. Again we use the DataLayer
class to set the selected title and then navigate to the DvdDetails
page, passing through a string
parameter.
void TitlesListBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var selectedDvd = ((ListBox)sender).SelectedItem as Title;
if (selectedDvd == null)
{
return;
}
string titleId = selectedDvd.Id;
DataLayer.SetSelectedTitle(titleId, selectedDvd);
NavigationService.Navigate(new Uri
("/DvdDetails.xaml?Id=" + titleId, UriKind.Relative));
((ListBox)sender).SelectedItem = null;
}
The difference is that when we navigate to the DvdDetails
page, we set the DataContext
to the selected title (retrieved from the DataLayer
class) directly in the OnNavigatedTo
method.
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
string titleId;
if (NavigationContext.QueryString.TryGetValue("Id", out titleId))
{
DataContext = DataLayer.GetSelectedTitle(titleId);
}
}
Saving and Restoring Transient State
Although this is not fully demonstrated in the demo application, saving an application’s transient state is an important part of developing Windows Phone applications. I’ve learned about this from reading Daniel’s chapters on the Windows Phone application life-cycle, and will summarise a part of it here.
Whenever your application is interrupted, it may go into a state called tombstoned. Tombstoned means that your application has been terminated, but that it may be re-activated later. When tombstoned, you need to save the current state of the application, so that when the application is re-activated, it will appear to the user as though nothing has happened.
The DataLayer
class, which in the demo is used mainly to save and retrieve objects, can be used by the application to save and restore its state across pages and whenever the application is deactivated:
public class DataLayer
{
static Dictionary<string, DataServiceCollection<Title>> titlesDictionary
= new Dictionary<string, DataServiceCollection<Title>>();
static Dictionary<string, Title> titleDictionary = new Dictionary<string, Title>();
const string titlesDictionaryKey = "DataLayer.titlesDictionary";
const string titleDictionaryKey = "DataLayer.titleDictionary";
public static void LoadFromDictionary(IDictionary<string, object> stateDictionary)
{
titlesDictionary = (Dictionary<string,
DataServiceCollection<Title>>)stateDictionary[titlesDictionaryKey];
titleDictionary = (Dictionary<string, Title>)stateDictionary[titleDictionaryKey];
}
public static void SaveToDictionary(IDictionary<string, object> stateDictionary)
{
stateDictionary[titlesDictionaryKey] = titlesDictionary;
stateDictionary[titleDictionaryKey] = titleDictionary;
}
public static void SetTitlesForGenre(string genreName,
DataServiceCollection<Title> titles)
{
titlesDictionary[genreName] = titles;
}
public static DataServiceCollection<Title> GetTitlesForGenre(string genreName)
{
return titlesDictionary[genreName];
}
public static void SetSelectedTitle(string id, Title title)
{
titleDictionary[id] = title;
}
public static Title GetSelectedTitle(string id)
{
return titleDictionary[id];
}
}
When the application is deactivated, the App
class calls the DataLayer.SaveToDictionary
method. This saves the transient state of DataLayer
class to a state Dictionary
that will survive if the application is tombstoned.
private void Application_Deactivated(object sender, DeactivatedEventArgs e)
{
IDictionary<string> dictionary = PhoneApplicationService.Current.State;
DataLayer.SaveToDictionary(dictionary);
}
If and when the application is re-activated, the App
class calls the DataLayer.LoadFromDictionary
method, which restores the state of the DataLayer
class:
private void Application_Activated(object sender, ActivatedEventArgs e)
{
var dictionary = PhoneApplicationService.Current.State;
DataLayer.LoadFromDictionary(dictionary);
}
Note that I haven’t saved the result of the OData calls in the demo application, but it’s something that should be done.
Progress Bar
The ProgressBar
control supported on the phone is the Silverlight ProgressBar
included in the System.Windows.Controls
namespace. A progress bar is used to indicate the progress of a lengthy task. It can either be value-based or indeterminate. The value-based ProgressBar
indicates the progress of a task based on the beginning and end point values (0 and 100 by default), whereas the indeterminate ProgressBar
displays a repeating animation that runs until the activity is completed (see Figure 4). The indeterminate ProgressBar
is attained by setting its IsIndeterminate
property to true
. In the demo, we use the indeterminate ProgressBar
on the GenreDvds
to indicate that something is happening while the genre’s titles are loading. For the purpose of the demo, I used the built-in ProgressBar
control, but if you are building a more serious application, which requires the indeterminate ProgressBar
, you should read Jeff Wilcox’s post and use his PerformanceProgressBar
instead. This is because the built-in version is costly, performance wise.
Figure 4: Indeterminate progress bar
Improving ProgressBar Indeterminate Mode Efficiency
As the following excerpt shows, the ProgressBar
control is placed inside a StackPanel
whose Visibility
property is data bound to the viewmodel
's Loaded
property. I used Daniel’s IValueConverter
, called BooleanToVisibilityConverter
, to convert the boolean Loaded
property to a Visibility
type. As per Jeff’s Wilcox’s advice, the IsIndeterminate
property of the ProgressBar
control is not directly set as true
but rather is data bound to the viewmodel
s' Busy
property to ensure that the repeating animation is properly turned off. The Visibility
property of the ProgressBar
is again databound to the viewmodel
’s Loaded
property which is converted to the Visibility
type via the BooleanToVisibilityConverter
that I borrowed from Daniel.
<StackPanel Grid.Row="1"
Visibility="{Binding Loaded,
Converter={StaticResource BooleanToVisibilityConverter},
ConverterParameter=Collapsed}"
Height="150" >
<TextBlock Text="Loading..." Style="{StaticResource PhoneTextTitle2Style}"
HorizontalAlignment="Center" Margin="20"/>
<ProgressBar IsIndeterminate="{Binding Busy}"
Visibility="{Binding Loaded,
Converter={StaticResource BooleanToVisibilityConverter},
ConverterParameter=Collapsed}" />
</StackPanel>
Conclusion
In this article, we looked at the Open Data protocol and showed how to consume OData in a Windows Phone application. We also explored the Silverlight Toolkit’s WrapPanel
control, looked at page navigation, and at the ProgressBar
control. I hope you found this article useful. If you enjoyed my article, please rate it and maybe share your thoughts below.
History
- November 2010: Initial release
References