Building AgiliTrain's Workshop Application for WP7


Windows Phone 7

Now that the Windows Phone 7 Tools are fully released, I sit here waiting for the phones to be released. In the time between the tool release and the phones being released, I have some precious time to get my applications ready for release as well. While I am not building any mass-market applications (nope, no fart apps here); I am building a number of small applications that should show off some features of the phone.

While many of you have seen my Moon Phaser application, I wanted to do something that was more in line with a typical phone application so I built an app to review my company's training offerings. The goal was simple: allow users to see the classes we're giving and learn about the the classes (with hopes of signing up ;).

I had started this application before the the Windows Phone 7 Tools were complete so I started niavely building a panorama-type application:

Sketch of AgiliTrain App

(click image for full size)

The idea was to have a list of apps in the Workshop pane and once a workshop was chosen, the other three panes would fill with data.  I got pretty far with this idea (not using the Panorama pane but instead using the Visual State Manager to move the page around). Once the tools were delivered I switched this to use a Panorama control instead. While this worked, the user experience was completely broken. If a user flicked right, there would be no data until you picked a workshop.  It occurred to me that this design was completely off-base. I was trying to shoe-horn the Panorama style into my own application. I would have designed this type of app on the desktop as a 'explorer-like' interface (click on the left-hand list/tree view and show detail data). But that doesn't apply itself onto the panorama as much as you might think. The Panorama design I think works better for generalized hubs where you have different classes of functionality or data in different categories.  One pane relying on the others is a broken user experience IMHO. Of course, who knows...we're early into the Metro style so I could be completely wrong.

In changing my application, I decided to go with page navigation and a pivot control. Essentially, the initial view of a workshop picker and then a second page with a pivot control for more information.  I decided that the picker should be really simple with title, list of workshops and a logo:

AgiliTrain Workshop Picker

Once the user picked a class, they could use a new page with a pivot to look at a mix of data:

AgiliTrain Workshop Picker

I am pretty happy with the result.  Here is a video of the app running on a device over WiFi:

My biggest challenge was to realize that this app was running on the phone. I know that seems obvious, but as a server, web and desktop developer I have learned that memory footprint isn't the most important thing (though it always is a consideration. For the phone this becomes a huge deal. In fact, according the the Applicaiton Certification Requirements (e.g. the rules of the Marketplace), your application can't take too much memory (emphasis mine):

5.2.5 Memory Consumption
The application must not exceed 90 MB of RAM usage. However, on devices that have more than 256 MB of memory, an application can exceed 90 MB of RAM usage. The DeviceExtendedProperties class can be used to query the amount of memory on the device and modify the application behavior at runtime to take advantage of additional memory. For more information, see the DeviceExtendedProperties class in MSDN.

Of course the amount of data I was initially bringing down was not trivial.  I was pulling down quite a lot of my object model on the server.  Before I worried too much, I decided to take a look at the memory consuption. As the guidelines say, the DeviceExtendedProperties class is the trick to measuring this for your application. Since I was already using a placeholder for errors, I decided that under the debug settings I'd use a polling timer to show the current memory in megabytes to see what my consuption was:

#if DEBUG
  var timer = new DispatcherTimer() { Interval = TimeSpan.FromSeconds(10) };
  timer.Tick += (s, e) =>
  {
    try
    {
      var memoryString = 
        DeviceExtendedProperties.GetValue("ApplicationCurrentMemoryUsage");
        
      var memoryInMB = Convert.ToInt32(memoryString) / (1024 * 1024.0);
      
      ErrorMessage = string.Format("Memory Usage: {0:n} MB", memoryInMB);
    }
    catch
    {
      // NOOP
    }
  };
  timer.Start();
#endif

You'll notice that I am calling DeviceExtendedProperties.GetValue to get the current memory usage of my application.  There are values for application memory and application peak memory.  The full set of extended properties are listed here:

http://msdn.microsoft.com/en-us/library/ff941122(v=VS.92).aspx

You can see the memory usage shown in the application here (note, this won't make it to the release version):

Memory Consuption in the Emulator 

On my initial checks of this, the memory consuption was huge (as I was pulling down all that information for all my workshops. This was causing a problem for both the bandwidth usage (e.g. networking speed) but also the memory consumption.  I realized that users would likely not look at all the workshops so pre-loading all the data wasn't helpful.  So I decided to split it up into three separate sections of data.  But first, how was I getting that data you might be wondering?

To get the data, I decided to use my publically available OData feed (e.g. WCF Data Services).  I decided to use this so I wouldn't have to have any server code at all.  The public feed exposes all the data I needed so it should just work.  The first step was figuring out how to consume OData on the phone.  There is a CTP version of the OData WP7 library that exists on OData.org:

http://www.odata.org/developers/odata-sdk

While the WP7 library works, it has a lot of limitations right now. The first thing you'll notice is that Visual Studio doesn't support "Add Service Reference..." so you'll have to build one using the command-line tooling as explained in my previous post here:

http://wildermuth.com/2010/08/09/Using_OData_with_Windows_Phone_7_SDK_Beta

The other limitation you'll see is that the LINQ syntax isn't working right.  So you'll have to execute your queries via the URI query syntax:

var qryUri = string.Format("/Events?$filter=Workshop/WorkshopId eq {0}", 
                                workshop.WorkshopId);
                                
Ctx.BeginExecute(new Uri(qryUri, UriKind.Relative), r>
  {
    try
    {
      var results = Ctx.EndExecute<Event>(r);
    }
    catch (Exception ex)
    {
      // TODO
    }
  }, null);

If you are an OData veteran, you might have gotten used to the LINQ syntax.  I expect this to make a return, but in this early version it doesn't work and might not in the supported release that is coming soon.  There are limitations on the platform (reflection emit is the big problem) that makes that much harder.

The last thing I had to deal with was that consuming this data was a challenge for a variety of reasons. The data that I have in the service was primarily designed for use on an HTML page therefore some of the data was formatted as HTML. This wouldn't be too tricky if Silverlight for the Windows Phone 7 contained the RichTextBox from Silverlight 4, but it doesn't so I opted for a simple converter that stripped the HTML out.

For the outline (which is really based on two related tables, not a HTML-based outline list) it was easier with nested DataTemplates:

<UserControl.Resources>
  <DataTemplate x:Key="subTopicTemplate">
    <StackPanel Orientation="Horizontal"
                Margin="8 0">
      <Ellipse Width="5"
                Height="5"
                Margin="2 0 4 0"
                Fill="{StaticResource PhoneAccentBrush}" />
      <TextBlock Text="{Binding Subtopic}"
                  TextWrapping="Wrap" />
    </StackPanel>
  </DataTemplate>
  <DataTemplate x:Key="topicTemplate">
    <StackPanel Margin="0 4">
      <TextBlock Text="{Binding Topic}"
                  TextWrapping="Wrap"
                  Foreground="{StaticResource PhoneAccentBrush}"/>
      <ItemsControl ItemsSource="{Binding WorkshopSubtopics}"
                    ItemTemplate="{StaticResource subTopicTemplate}" />
    </StackPanel>
  </DataTemplate>
</UserControl.Resources>

I find that using I used the ItemsControl a lot more since it doesn't have user interface chrome around it but is simply an easy 'repeater'.

Since I was downloading all that data, I wanted to keep it around as state.  Unfortunately the proxy created via the tools was using the DataServiceCollection class which doesn't play well with the serialization scheme using for tombstoning. (If you're not familiar with that phrase, see my blog entry on it here). I tried creating the OData proxy without it but there were other issues so I backtracked and edited the proxy by hand.  I simply replaced all DataServiceCollection<T> references with List<T>. I also had to use the IgnoreDataMember attribute for some utility properties I added on the data objects:

public partial class Workshop
{
  [IgnoreDataMember]
  public string Link
  {
    get
    {
      return string.Concat("https://agilitrain.com/Workshop/Outline/", 
                           Name.Replace(" ", "_"));
    }
  }
  ...
}

While this fixed the tombstone serialization issues, I found that when I used the WebBrowserTask (or perhaps other tasks too) that when returning to the application that it was locking up during re-hydrating the objects.  I am still not sure why not but I've sent MS some information that I hope will help. Currently I chose to not save the data during tombstoning and just reload it from the web.

The last thing I want to mention is that when I refactored it to use the page navigation I had a good idea.  I'd use a query string on the navigation so I could determine what information I needed in the details page. This is a pretty good idea and in fact there are several projects that are creating routing frameworks (like the one in ASP.NET and RoR) to help with this. But in this early pass, I decided to handle it myself.  But the problem was that the Uri class doens't parse query strings (or other parts) of relative URI's so I had to hack it manually.  Not hard, but annoying:

protected override void OnNavigatedTo(NavigationEventArgs e)
{
  base.OnNavigatedTo(e);

  ...

  // Hack together a parsing a query string.
  var url = e.Uri.OriginalString;
  var qry = url.Substring(url.IndexOf('?') + 1);
  var pieces = qry.Split('=');
  var id = int.Parse(pieces[1]);

  ...

}

This way I could retrieve the ID of the Workshop to display. I expect to use a routing framework as time goes on, but in this early stage I am ok with a little hacking to get it to work.

I hope that my story of building this app will help you see some of the changes you need to think about when designing your phone applications and you won't just simply try to port desktop/web apps to the phone. The promise is there to have a single codebase for all three but its not realistic to me.  The difference in the touch interface, screensize and smaller device (e.g. CPU and memory) means that its an entirely different experience on the phone. You can re-use assets that you have (e.g. like my OData service) but the experience design is completely changed.

What do you think?

Shameless Plug:

If you are building phone apps and you need some help doing so, don't forget that I am teaching two Silverlight for the Windows Phone 7 Workshops coming up before the end of the year for my company AgiliTrain:

 



Shawn
Shawn Wildermuth
Author, Teacher, and Coach




My Courses

Wilder Minds Training
Vue.js by Example (New Lower Price)
Bootstrap 4 by Example (New Lower Price)
Intro to Font Awesome 5 (Free Course)
Pluralsight
Building an API with ASP.NET Core (New Course)
Building a Web App with ASP.NET Core, MVC6, EF Core, Bootstrap and Angular (updated for 2.2)
Less: Getting Started (New)
Using Visual Studio Code for ASP.NET Core Projects
Implementing ASP.NET Web API

Application Name WilderBlog Environment Name Production
Application Ver v4.0.30319 Runtime Framework x86
App Path D:\home\site\wwwroot\ Runtime Version .NET Core 4.6.27317.03
Operating System Microsoft Windows 10.0.14393 Runtime Arch X86