Windows Phone 7: Navigation

DIGG For this tutorial we’re going to, again,  partially recreate one of the excellent labs available in the Windows Phone 7 Developer Training Kit.

To begin, I created an application called (not my fault!) Wazup and added the existing assets from the lab: specifically,

  • WazupAppIcon.png
  • WazupStartIcon.png
  • Styles.xaml

All three are added using Add->Existing Item.

The two png files are plugged in through the WMAppManifest.xml file (under properties)

<IconPath IsRelative="true" IsResource="false">
         WazupIcon.png</IconPath>

<BackgroundImageURI IsRelative="true" IsResource="false">
     WazupStartIcon.png</BackgroundImageURI>

And the Styles is linked in by opening App.xaml and adding the resource dictionary,

 <Application.Resources>
    <ResourceDictionary>
       <ResourceDictionary.MergedDictionaries>
          <ResourceDictionary
             Source="Styles.xaml" />
       </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
 </Application.Resources>

SourceResources There are a number of images we’d like to add to the project, and that were created by the folks who built the lab, so let’s grab those and drop them into a folder we’ll create and name Resources. Change the Build Action on these images and the previous two to Content, and change the Copy to Output Directory property to Copy if Newer.

Changing the Build Action to Content ensures that the resources will be compiled into the .xap file, making the xap file bigger but avoiding the addition of a dll.

The next step is to create a View folder, and in that folder to create a new Windows Phone Portrait Page named TrendsPage.xaml

Creating The UI

Open Main.xaml and fix up the title for the application and the page. At this point you want to add three buttons and an image. The image is in your Resources folder (assuming you followed the steps above).  To do so, create a StackPanel, and use three Button objects and an Image.

<Grid x:Name="ContentPanel"
   Grid.Row="1" Margin="12,0,12,0">
 <StackPanel>
    <Button
       x:Name="ButtonDigg"
       Content="Digg"/>
    <Button
       x:Name="ButtonTwitter"
       Content="Twitter Trends"/>
    <Button
       x:Name="ButtonBlog"
       Content="Windows Phone Blog"/>
    <Image
       Height="180"
       Source="/Resources/Logos.png"
       Stretch="None"
       Margin="0,40,0,0"
       HorizontalAlignment="Center" />
 </StackPanel>
</Grid>

Creating A Navigation Helper

While not strictly necessary, we can make navigation easier by factoring out a lot of the common code into a Navigation class.  We’ll create an enumeration of the pages we might want to go to, and then factor out the use of the Page’s access to the Navigation Service’s Navigate method (which takes a URI to the page you want to navigate to.)

public enum ApplicationPages
{
   Digg,
   Twitter,
   Blog
}
public class Navigation
{
   private static void GoToPage( PhoneApplicationPage phoneApplicationPage, ApplicationPages applicationPage )
   {
      switch ( applicationPage )
      {
         case ApplicationPages.Digg:
            phoneApplicationPage.NavigationService.Navigate(
                  new Uri( "/Views/DiggPage.xaml",
                          UriKind.Relative ) );
            break;
         case ApplicationPages.Blog:
            phoneApplicationPage.NavigationService.Navigate(
                  new Uri( "/Views/BlogPage.xaml",
                         UriKind.Relative ) );
            break;
         case ApplicationPages.Twitter:
            phoneApplicationPage.NavigationService.Navigate(
                 new Uri( "/Views/TwitterPage.xaml",
                     UriKind.Relative ) );
            break;
      }
   }
}

This allows the event handlers in MainPage.cs to be very simple,

 void ButtonBlog_Click( object sender, RoutedEventArgs e )
 {
    Navigation.GoToPage( this, ApplicationPages.Blog );
 }

 void ButtonTwitter_Click( object sender, RoutedEventArgs e )
 {
    Navigation.GoToPage( this, ApplicationPages.Twitter );
 }

 void ButtonDigg_Click( object sender, RoutedEventArgs e )
 {
    Navigation.GoToPage( this, ApplicationPages.Digg );
 }

ViewPages Before running this we need to make sure that there are in fact the three pages in a Views folder (!)  Create the folder and put the three pages in, but for now we won’t add anything to the pages except to set the PageTitle so that we can test that we are navigating correctly.

Run the application, click on each of the three buttons and the correct page should appear, but not do a whole lot.

Digg Page Functionality

Let’s implement the Digg Page’s functionality, specifically with a focus on saving and restoring page state when navigating away from the application (see tutorial on Tombstoning).

The Digg page has a TextBox that contains the text to be searched, a search Button, a TextBlock that presents the last searched text, a ListBox that will present the search results.

The ListBox’s ItemsSource property will be bound to a collectgion of DiggStory items. We’ll create an ItemTemplate to properly display information from each story.

Start by defining three rows and two columns for the content panel, then add the TextBox, Button and TextBlock.

<Grid
   x:Name="ContentPanel"
   Margin="12,0,12,0" Grid.Row="1">
   <Grid.RowDefinitions>
      <RowDefinition
         Height="Auto" />
      <RowDefinition
         Height="Auto" />
      <RowDefinition
         Height="*" />
   </Grid.RowDefinitions>

   <Grid.ColumnDefinitions>
      <ColumnDefinition
         Width="*" />
      <ColumnDefinition
         Width="Auto" />
   </Grid.ColumnDefinitions>
   <TextBox
      x:Name="TextBoxSearch"
      Grid.Column="0"
      Text="{Binding SearchText, Mode=TwoWay}" />
   <Button
      x:Name="ButtonSearch"
      Grid.Column="1"
      Click="ButtonSearch_Click"
      VerticalAlignment="Top"
      Padding="0"
      Style="{StaticResource ButtonGoStyle}" />
   <TextBlock
      x:Name="TextBlock"
      Grid.Row="1"
      Grid.ColumnSpan="2"
      Text="{Binding LastSearchText}"
      Margin="18,0,0,5"
      HorizontalAlignment="Left"
      FontSize="24"
      Height="45" />
</Grid>

We’d like to have a horizontal rule below the search box. You can accomplish that with a Path element. You give it the beginning location and the ending position of the line,

<Path
   Grid.ColumnSpan="2"
   Data="M0,80 L448,80"
   Height="1"
   Margin="0,2,12,4"
   Grid.Row="1"
   Stretch="Fill"
   Stroke="#B2FFFFFF"
   UseLayoutRounding="False"
   VerticalAlignment="Bottom" />

The stroke property sets the colo9r of the line, the margin, alignment and height are used for positioning.  Finally, we end with the ListBox.

<ListBox
   Grid.Row="2"
   Grid.ColumnSpan="2"
   ItemsSource="{Binding DiggSearchResults}"
   ItemTemplate="{StaticResource DiggSearchResultTemplate}" />

For all of the above controls, the styles defined in StaticResources have been in the Styles.xaml file we added earlier. This time however, we need the ItemTemplate, which we’ll put at the top of this page,

<phone:PhoneApplicationPage.Resources>
   <DataTemplate
      x:Key="DiggSearchResultTemplate">
      <Grid
         Margin="0">
         <Grid.RowDefinitions>
            <RowDefinition
               Height="Auto" />
            <RowDefinition
               Height="Auto" />
            <RowDefinition
               Height="Auto" />
         </Grid.RowDefinitions>
         <Grid.ColumnDefinitions>
            <ColumnDefinition
               Width="*" />
         </Grid.ColumnDefinitions>
         <StackPanel
            Background="#FF27580A"
            HorizontalAlignment="Left"
            Height="35"
            Margin="0,7,0,0"
            VerticalAlignment="Top"
            Orientation="Horizontal">

            <TextBlock
               Text="{Binding Diggs}"
               Foreground="White"
               Margin="5,2,5,3"
               HorizontalAlignment="Left"
               VerticalAlignment="Bottom" />
            <TextBlock
               TextWrapping="Wrap"
               Foreground="White"
               Text="diggs"
               d:LayoutOverrides="Width"
               HorizontalAlignment="Left"
               VerticalAlignment="Bottom"
               FontSize="16"
               Margin="0,2,5,3" />
         </StackPanel>
         <HyperlinkButton
            Grid.Column="0"
            Content="{Binding Title}"
            NavigateUri="{Binding Link}"
            TargetName="_blank"
            FontSize="29.333"
            Foreground="#FFFFC425"
            HorizontalAlignment="Left"
            Style="{StaticResource WrappedHyperlinkButtonStyle}"
            HorizontalContentAlignment="Left"
            Grid.Row="1"
            Margin="0,0,0,3" />
         <TextBlock
            Grid.Row="2"
            Grid.ColumnSpan="2"
            Text="{Binding Description}"
            Foreground="White"
            TextWrapping="Wrap"
            Margin="0,0,12,35"
            HorizontalAlignment="Left" />
      </Grid>
   </DataTemplate>
</phone:PhoneApplicationPage.Resources>

Adding Dependency Properties

The only thing you can bind to is a dependency property, so our DiggPage needs to create a dependency property for the search text.  That will be exposed through a standard public property SearchText.

 public static readonly DependencyProperty SearchTextProperty =
   DependencyProperty.Register(
   "SearchText",
   typeof( string ),
   typeof( DiggPage ),
   new PropertyMetadata( ( string ) "" ) );

Dependency Properties must be registered, and the syntax for registration is to pass in

  • the name of the property you are registering
  • the type of the property you’re registering
  • the type of the object that is registering the property
  • an instance of PropertyMetaData, used to provide the dependency property with essential functionality (e.g., getting its type, determining equality, etc.)

Here’s the public property that exposes the dependency property,

public string SearchText
{
   get { return ( string ) GetValue( SearchTextProperty ); }
   set { SetValue( SearchTextProperty, value ); }
}

Notice that the value of dependency properties is returned and set through GetValue and SetValue respectively. The other dependency properties that we’ll need are LastSearchText, DiggSearchResults and IsSearching,

In The Constructor

We have two jobs to accomplish in the constructor: to set the data context for the binding, and to set the event handler for the button:

public DiggPage()
{
   InitializeComponent();
   ButtonSearch.Click += ButtonSearch_Click;
   LayoutRoot.DataContext = this;
}

DiggStory and DiggService

Before we can try to compile, however, we have to define a DiggStory and a DiggService.  The DiggStory is a straightforward object built on the content we can expect from the service.  Create a folder, Services, and place the DiggStory class in that folder. The job of this class is to manage serialization of the story, and so you’ll need to add a reference (from the .NET tab) of System.Runtime.Serialization.  Here’s the complete source for DiggStory,

using System.Runtime.Serialization;

namespace Wazup.Services
{
   [DataContract]
   public class DiggStory
   {
      public DiggStory( string title, string description, string link, int diggs )
      {
         Title = title;
         Description = description;
         Link = link;
         Diggs = diggs;
      }

      [DataMember]
      public string Title { get; set; }

      [DataMember]
      public string Description { get; set; }

      [DataMember]
      public string Link { get; set; }

      [DataMember]
      public int Diggs { get; set; }
   }
}

Interacting with Digg Via WebClient

The easiest way to exchange data with a resource that can be identified by a URI is to use a WebClient.  The key methods of WebClient, for exchanging data, are DownloadStringAsync and OpenReadAsync.  It is the former that we’ll use here,

public static void Search(
   string searchText,
   Action<IEnumerable<DiggStory>> onSearchCompleted = null,
   Action<string, Exception> onError = null,
   Action onFinally = null )
{
   WebClient webClient = new WebClient();
   //...
   webClient.DownloadStringAsync(
         new Uri(
         string.Format(
         DiggSearchQuery,
         searchText,
         Wazup_DiggApplicationKey ) ) );
}
The Actions shown in the signature are a short hand for a delegate that returns void, and simplify the code by removing the necessity of creating and using a named, one-time delegate

All that is missing form this code is the implementation of DownloadStringCompleted (remembering that DownloadStringAsync is, as the name implies, asynchronous,

 public static void Search(
    string searchText,
    Action<IEnumerable<DiggStory>> onSearchCompleted = null,
    Action<string, Exception> onError = null,
    Action onFinally = null )
 {
    WebClient webClient = new WebClient();

    webClient.DownloadStringCompleted += delegate(
      object sender,
      DownloadStringCompletedEventArgs e )
    {
       try
       {
          if ( e.Error != null )
          {
             if ( onError != null )
             {
                onError( searchText, e.Error );
             }
             return;
          }

          XElement storyXml = XElement.Parse( e.Result );

          var stories = from story in storyXml.Descendants(
                                                "story" )
             select new DiggStory(
               story.Element( "title" ).Value,
               story.Element( "description" ).Value,
               story.Attribute( "link" ).Value,
               int.Parse( story.Attribute( "diggs" ).Value ) );

          if ( onSearchCompleted != null )
          {
             onSearchCompleted( stories );
          }
       }
       finally
       {
          if ( onFinally != null )
          {
             onFinally();
          }
       }
    };

    webClient.DownloadStringAsync( new Uri( string.Format( DiggSearchQuery, searchText, Wazup_DiggApplicationKey ) ) );

 }

Note that the assignment to DownloadStringCompleted can be simplified considerably by using Linq syntax,

 webClient.DownloadStringCompleted += ( sender, e ) =>
 {
    try
    {
       if ( e.Error != null )
       {
          if ( onError != null )
             onError( searchText, e.Error );
          return;
       }
       XElement storyXml = XElement.Parse( e.Result );
       var stories = from story in storyXml.Descendants( "story" )
                     select new DiggStory(
                        story.Element( "title" ).Value,
                        story.Element( "description" ).Value,
                        story.Attribute( "link" ).Value,
                        int.Parse( story.Attribute( "diggs" ).Value ) );
       if ( onSearchCompleted != null )
          onSearchCompleted( stories );
    }
    finally
    {
       if ( onFinally != null )
          onFinally();
    }
 };

The DiggPage Completed

First task is to fill in the code for what happens when the Search button is  pressed,

void ButtonSearch_Click( object sender, RoutedEventArgs e )
{
   if ( string.IsNullOrEmpty( SearchText ) )
   {
      return;
   }

   IsSearching = true;
   DiggService.Search( SearchText,
       delegate( IEnumerable<DiggStory> diggSearchResults )
       {
          IsSearching = false;

          LastSearchText = SearchText;

          DiggSearchResults = new ObservableCollection<DiggStory>();

          foreach ( DiggStory diggSearchResult in diggSearchResults )
          {
             DiggSearchResults.Add( diggSearchResult );
          }
       },
       delegate( string searchText, Exception exception )
       {
          IsSearching = false;
          LastSearchText = string.Format(
               "Error while searching {0}.", searchText );
          System.Diagnostics.Debug.WriteLine( exception );
       } );
}

We grab the search string and call Search on the DiggService, and we iterate through the search results adding each story in turn.

To close the loop we need to handle two more events, one traiggered when we navigate away from this page, and the other when we navigate to this page. In these cases we want to store and retrieve the state of the page respectively.  To do so we must first pause, and create a helper class, stateManager.cs, which looks like this,

 public static class StateManager
 {
    public static void SaveState(
         this PhoneApplicationPage phoneApplicationPage,
         string key, object value )
    {
       if ( phoneApplicationPage.State.ContainsKey( key ) )
       {
          phoneApplicationPage.State.Remove( key );
       }

       phoneApplicationPage.State.Add( key, value );
    }

    public static T LoadState<T>(
       this PhoneApplicationPage phoneApplicationPage,
       string key )
        where T : class
    {
       if ( phoneApplicationPage.State.ContainsKey( key ) )
       {
          return ( T ) phoneApplicationPage.State[ key ];
       }

       return default( T );
    }
 }

we can now finish out the DiggPage,

 private const string SearchTextKey = "SearchTextKey";
 private const string LastSearchTextKey = "LastSearchTextKey";
 private const string DiggSearchResultsKey = "DiggSearchResultsKey";

 protected override void OnNavigatedFrom( System.Windows.Navigation.NavigationEventArgs e )
 {
    this.SaveState( SearchTextKey, SearchText );
    this.SaveState( LastSearchTextKey, LastSearchText );
    this.SaveState( DiggSearchResultsKey, DiggSearchResults );
 }

 protected override void OnNavigatedTo( System.Windows.Navigation.NavigationEventArgs e )
 {
    SearchText = this.LoadState<string>( SearchTextKey );
    LastSearchText = this.LoadState<string>( LastSearchTextKey );
    DiggSearchResults = this.LoadState<ObservableCollection<DiggStory>>( DiggSearchResultsKey );
 }

To avoid any confusion, here (collapsed) is the entire DiggPage,

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Windows;
using Microsoft.Phone.Controls;
using Wazup.Services;

namespace Wazup.Views
{
   public partial class DiggPage : PhoneApplicationPage
   {
      public DiggPage()
      {
         InitializeComponent();
         ButtonSearch.Click += ButtonSearch_Click;
         LayoutRoot.DataContext = this;
      }

      void ButtonSearch_Click( object sender, RoutedEventArgs e )
      {
         if ( string.IsNullOrEmpty( SearchText ) )
         {
            return;
         }

         IsSearching = true;
         DiggService.Search( SearchText,
             delegate( IEnumerable<DiggStory> diggSearchResults )
             {
                IsSearching = false;

                LastSearchText = SearchText;

                DiggSearchResults = new ObservableCollection<DiggStory>();

                foreach ( DiggStory diggSearchResult in diggSearchResults )
                {
                   DiggSearchResults.Add( diggSearchResult );
                }
             },
             delegate( string searchText, Exception exception )
             {
                IsSearching = false;
                LastSearchText = string.Format( "Error while searching {0}.", searchText );
                System.Diagnostics.Debug.WriteLine( exception );
             } );

      }

      public static readonly DependencyProperty SearchTextProperty =
        DependencyProperty.Register(
        "SearchText",
        typeof( string ),
        typeof( DiggPage ),
        new PropertyMetadata( ( string ) "" ) );

      public string SearchText
      {
         get { return ( string ) GetValue( SearchTextProperty ); }
         set { SetValue( SearchTextProperty, value ); }
      }

      public static readonly DependencyProperty LastSearchTextProperty =
          DependencyProperty.Register( "LastSearchText", typeof( string ), typeof( DiggPage ),
              new PropertyMetadata( ( string ) "" ) );

      public string LastSearchText
      {
         get { return ( string ) GetValue( LastSearchTextProperty ); }
         set { SetValue( LastSearchTextProperty, value ); }
      }

      public static readonly DependencyProperty DiggSearchResultsProperty =
          DependencyProperty.Register( "DiggSearchResults", typeof( ObservableCollection<DiggStory> ), typeof( DiggPage ),
              new PropertyMetadata( ( ObservableCollection<DiggStory> ) null ) );

      public ObservableCollection<DiggStory> DiggSearchResults
      {
         get { return ( ObservableCollection<DiggStory> ) GetValue( DiggSearchResultsProperty ); }
         set { SetValue( DiggSearchResultsProperty, value ); }
      }

      public static readonly DependencyProperty IsSearchingProperty =
          DependencyProperty.Register( "IsSearching", typeof( bool ), typeof( DiggPage ),
              new PropertyMetadata( ( bool ) false ) );

      public bool IsSearching
      {
         get { return ( bool ) GetValue( IsSearchingProperty ); }
         set { SetValue( IsSearchingProperty, value ); }
      }

      private const string SearchTextKey = "SearchTextKey";
      private const string LastSearchTextKey = "LastSearchTextKey";
      private const string DiggSearchResultsKey = "DiggSearchResultsKey";

      protected override void OnNavigatedFrom( System.Windows.Navigation.NavigationEventArgs e )
      {
         this.SaveState( SearchTextKey, SearchText );
         this.SaveState( LastSearchTextKey, LastSearchText );
         this.SaveState( DiggSearchResultsKey, DiggSearchResults );
      }

      protected override void OnNavigatedTo( System.Windows.Navigation.NavigationEventArgs e )
      {
         SearchText = this.LoadState<string>( SearchTextKey );
         LastSearchText = this.LoadState<string>( LastSearchTextKey );
         DiggSearchResults = this.LoadState<ObservableCollection<DiggStory>>( DiggSearchResultsKey );
      }

   }
}

About Jesse Liberty

Jesse Liberty has three decades of experience writing and delivering software projects and is the author of 2 dozen books and a couple dozen online courses. His latest book, Building APIs with .NET will be released early in 2025. Liberty is a Senior SW Engineer for CNH and he was a Senior Technical Evangelist for Microsoft, a Distinguished Software Engineer for AT&T, a VP for Information Services for Citibank and a Software Architect for PBS. He is a Microsoft MVP.
This entry was posted in iPhoneToWP7, Patterns & Skills, WindowsPhone and tagged , . Bookmark the permalink.

6 Responses to Windows Phone 7: Navigation

Comments are closed.