Designing My First (Public) Windows Phone App

-or-

Inside the PASS Event Browser for Windows Phone (Part 1)

Now that I have completed the first update to my PASS Event Browser app for Windows Phone 7.5, I thought it might be helpful for me to share some of my experiences in this process—at least for folks considering creating OData client apps on the Windows Phone platform. In a previous post, I provided a functional overview of this Windows Phone app, which consumes the public PASS Events OData feed. In this post, I will cover OData-specific design decisions that I made. In a subsequent post I will share my thoughts on the certification and publishing process as well as my ideas for future improvements to my application.

Design Decisions

First, let’s discuss some of the design decisions that I was confronted with when I started on the app—keeping in mind that I was trying to get this application completed in time to be available for the PASS Summit 2011, which didn’t end up working out. If you want to see the actual source code of the PASS Events Browser, I’ve published the v1.1 code as the PASS Events OData Feed Browser App for Windows Phone 7.5 project on MSDN Code Gallery.

Choosing the Windows Phone Version

I managed to land myself right in the middle of the launch and subsequent rollout of Windows Phone 7.5 (“Mango”). Because of my concern that most PASS attendees wouldn’t have Mango on their phones in time (I didn’t have it either until I forced it onto my Samsung Focus a week before the Summit), I started out creating a Windows Phone 7 app using the OData client library for Windows Phone from codeplex—this despite the lack of VS integration and LINQ support. However, the real deal breaker was a bug in DataServiceState in this v1 library that prevented the proper serialization of nested binding collections. Once I hit this known issue, I quickly ported my code to the Mango codebase and the Windows Phone SDK 7.1, which includes the full support for LINQ and integration with Add Service Reference in Visual Studio.

Page Design

The folks at PASS headquarters were gracious enough to allow me to leverage their rainbow swish graphic from the PASS Summit 2011 site. With this, I was able to implement some rather nice (IMHO) Panorama controls on most pages. You can get the feel for how the Panorama control looks the following series of screen shots from the session details page:

image
Session details page Panorama control.

At the 11th hour, I also had to go back and create white versions of these background images to handle the “light-theme gotcha” to pass certification.

Data Binding

The best unit-of-work pattern for the application was for me to use a single DataServiceContext for the entire application execution. Because of this, I also ended up using a single, static ViewModel that exposes all of the DataServiceCollection<T> properties needed to binding the pages to events, sessions, and speakers. This view model also exposes a QueryInfo property that is used for binding the session filters, which are used when building session queries. Here’s a snap of the public members used for binding:

image
Public API of the MainViewModel.

Building Queries

The way the app works in v1 is that when you start, the PASS Events OData feed is queried, and all the events are bound to the Panorama control in the main page. Then, when you tap an event in the ListBox, event details are displayed in the event details page (Panorama). At the same time, all sessions for the selected event are loaded from the data service, and these are parsed once to get the possible values for the session filters.

SessionFilters
Session filters.

To filter the sessions, the event details page sets the QueryInfo property of the ViewModel, which is used by the ViewModel when composing the query for filtered sessions, which is built by the following code:

// Get a query for all sessions
var query = _context.Sessions;

// Add query filters.
if (this.QueryInfo.Category != null && !this.QueryInfo.Category.Equals(string.Empty))
{
query = query.Where(s => s.SessionCategory.Equals(this.QueryInfo.Category))
as DataServiceQuery<Session>;
}

if (this.QueryInfo.Track != null && !this.QueryInfo.Track.Equals(string.Empty))
{
query = query.Where(s => s.SessionTrack.Equals(this.QueryInfo.Track))
as DataServiceQuery<Session>;
}

if (this.QueryInfo.Level != null && !this.QueryInfo.Level.Equals(string.Empty))
{
query = query.Where(s => s.SessionLevel.Equals(this.QueryInfo.Level))
as DataServiceQuery<Session>;
}

if (this.QueryInfo.Date != null && !this.QueryInfo.Date.Equals(string.Empty))
{
DateTime date = DateTime.Parse(this.QueryInfo.Date);

    query = query.Where(s => s.SessionDateTimeStart < date.AddDays(1) &&
s.SessionDateTimeStart >= date) as DataServiceQuery<Session>;
}

if (this.QueryInfo.QueryString != null && !this.QueryInfo.QueryString.Equals(string.Empty))
{
// Search description and title.
query = query.Where(s => s.SessionDescription.Contains(this.QueryInfo.QueryString)
| s.SessionName.Contains(this.QueryInfo.QueryString)) as DataServiceQuery<Session>;
}

query = query.Where(s => s.EventID.Equals(CurrentEvent.EventID))
.OrderBy(s => s.SessionName) as DataServiceQuery<Session>;                             

// Exceute the filtering query asynchronously.
this.Sessions.LoadAsync(query);

In v1, session data is not cached locally, so subsequent queries to return a filtered set of sessions result in a filtered query being executed against the data service, with the selected session entities being downloaded again and bound to the sessions list.

Side Bar: the lack of local caching is because I wasn’t counting on having LINQ on the client—my original plan called for using Windows Phone 7 and not Mango, and I didn’t have time to address this in v1.

Loading Data

I discovered, after deploying the app to my actual device and trying to use it outside of my home network (the worst case for me on the AT&T network in Seattle was always ~3pm, which I always assumed was due to kids getting out of class), that loading all 200+ Summit 2011 sessions was a rather slow process, which was also blocking the UI thread. Since I was worried about this perceptible delay derailing my certification, I decided to implement client-side paging. By loading sessions in smaller chunks, the UI unblocks during the callbacks, making the app seem more responsive (at least to me). The following code in the LoadCompleted event handler loads individual session pages from the data service:

// Make sure that we load all pages of the Content feed.
if (Sessions.Continuation != null)
{
Sessions.LoadNextPartialSetAsync();
}

if (e.QueryOperationResponse.Query.RequestUri.Query.Contains("$inlinecount"))
{
if (e.QueryOperationResponse.Query.RequestUri.Query
.Contains("$inlinecount=allpages"))
{
// Increase the page count by one.
pageCount += 1;

        // This is the intial query for all sessions.
TotalSessionCount = (int)e.QueryOperationResponse.TotalCount;

        CountMessage = string.Format("{0} total sessions.", TotalSessionCount);
NotifyPropertyChanged("CountMessage");
}
if (Sessions.Count < TotalSessionCount)
{
try
{
// We need to distinguish a query for all sessions, so we use $inlinecount
// even when we don't need it returned.
var query = this.BuildSessionsQuery().AddQueryOption("$inlinecount", "none");

            // Load the next set of pages.
this.Sessions.LoadAsync(query);
}
catch (Exception ex)
{
this.Message = ex.Message.ToString();
}

        // Increase the page count by one.
pageCount += 1;

        // Load the session filter values in batches.
BuildSessionFilters();
}
else
{
// We are done loading pages.
this.Sessions.LoadCompleted -= OnSessionsLoaded;
IsSessionDataLoaded = true;
IsDataLoading = false;

        if (LoadCompleted != null)
{
LoadCompleted(this, new SourcesLoadCompletedEventArgs(e.Error));
}
}
}

In this version, I am not using paging for these filtered queries. Also, in an upcoming release, I plan to make the client page size configurable.

Maintaining State

The OData client provides the DataServiceState object to help with serialization and deserialization of objects in the context. The Mango version of DataServiceState actually works to correctly serialize and deserialize the context and binding collections (in v1, it’s very iffy), and this is the major reason why I upgraded the app to Mango for the v1.0 release.

Here is an important tip: don’t store entities in the state manager when they are also tracked by the context; otherwise you can get exceptions during serialization. This happens when Windows Phone tries to serialize entities with a property that returns a collection of related objects. Only the DataServiceState is able to correctly serialize objects in a DataServiceCollection<T>. If you need to maintain the state of individual tracked objects, then instead store the URI of the object, which is returned by the DataServiceContext.TryGetUri method. Later you can retrieve the object from the restored DataServiceContext by using the DataServiceContext.TryGetEntity<T> method. Here’s an example of how this works:

// We can't store entities directly, so store the URI instead.
if (CurrentSession != null && _context.TryGetUri(CurrentSession, out storageUri))
{
// Store the URI of the CurrentSession.
stateList.Add(new KeyValuePair<string, object>("CurrentSession", storageUri));
}

// Restore entity properties from the stored URI.
_context.TryGetEntity<Session>(storedState["CurrentSession"] as Uri, out _currentSession);

Toolkit Goodies

In addition to the Windows Phone SDK 7.1 (including the OData client for Windows Phone 7.5), I also used the Silverlight Toolkit in this application. In particular, I used the PerformanceProgressBar as my loading indicator (supposedly this has better performance than the ProgressBar control in Silverlight) and the ListPicker for my session filters.  I released v1.0 using the older February 2011 version of the toolkit, and when I upgraded to the Mango version for the 7.1 SDK for the v1.1 release, the ListPicker was broken. Now, I have to handle the Tap event to manually open the listpicker (hope they fix this in the next version).


Tune in next week for the exciting conclusion to this thrilling saga, including the joy and heartbreak of Marketplace certification and the future of this app…

Glenn Gailey