Click here to Skip to main content
Click here to Skip to main content

A Windows Phone 7 App from the Ground Up

, 21 Dec 2010 CPOL
Rate this:
Please Sign up or sign in to vote.
Building a WP7 browser app for last.fm

Introduction

After swearing of Windows phones a couple of years back or so I am back in the possession of nice shiny new HTC HD7. I've played with writing mobile apps in the past on the CE platform, and while that was fun they just never compared to what was possible on the iPhone and then Android platforms. I even went so far as to try and emulate on the iPhone look and feel on CE 6. The problem always was that CE just wasn't as nice as those other OS's and I could never bring myself to learn the iPhone or Android technology stacks (due to the combination of lack of time and very small brain). But now there's the Windows Phone 7 platform and based on my first couple of weeks of ownership it looks to be a real competitor. And best of all I can use C#, XAML, and all that other MS stuff I already know; without having to teach myself new tricks (well at least not completely new - there are challenges enough in the platform differences between WP7 and a desktop OS).

So this is my first crack at a Windows Phone 7 app. It is a browser for the online music service last.fm. Last.fm is a social media/music service that makes music recommendations and allows you to connect with people with similar taste. This app doesn't include streaming the music content they provide (because honestly I can't figure out whether I can legally do that or not) but it does provide access to much of the other content associated with a last.fm account.

It is up on the app hub if you like last.fm and want to use it.

Background

Prerequisites

There are some things you will need to have in order to play around with this code.

  • The WP7 SDK and associated tools - for obvious reasons
  • GalaSoft's MVVM Light - for MVVM support (Commands, ViewModel, Mediator etc.)
  • The Silverlight for Windows Phone Toolkit - for some additional controls like the WrapPanel
  • A last.fm API key - last.fm API access has to include a key signature which is associated with a particular person. They are easy enough to get and free. I've removed my keys form the source code and replaced them with #warning statements so you know where to plug yours in.
  • A BING Maps key - There are a couple of places where I use the Bing Maps control. This too requires a key and I've removed mine and noted where to plug yours in.
  • A last.fm account - there won't be much to look at unless you have an account with the last.fm service

There are other bits of code that I've harvested from the InterWebs. I'll point those out as we go along.

Using the code

The Data Layer and Model

So let's start with the data layer and model. Last.fm exposes their API as a set of RESTful services. The first thing that needed to be done is communication with those services. They provide the option of Xml or JSON responses. I chose to go with Xml mostly because I'm familiar with Xml and haven't dabbled in JSON as of yet.

Receiving Data

I started with an attempt to port LastFmSharp to Silverlight but it just started to get messy and never really worked. I then started looking to see if I could get RIA Rest Services to do the job. After poking around a bit without luck I tried the ServiceModel namespace. I'm sure there is a built in WCFy way to access the last.fm services but I couldn't get anything to work so I just decided to wire things up by hand. I'll tell you, without the types that "Right Click | Add Service Reference" spits out, it is not straightforward wiring up a REST service. The last.fm service does not provide WSDL-like meta data and the Xml structures that they serve up are not the cleanest (i.e. there structure seems somewhat variable depending on the method call. Sometimes an <artist> has one structure and sometimes a slightly different structure).

All in all wiring up a small Rest client from scratch was kind of fun and not all that difficult. Plus having complete control over how objects get deserialized allows me to deal with the peculiarities of the API.

So again, let's start at the very bottom: reading and writing data. Reads and writes are accomplished via RemoteMethod objects. The base class encapsulates the name and arguments of the remote method and the particular message signing that last.fm requires. Note: last.fm requires MD5 hashing of the messages. WP7 does not include the Md5CryptoServiceProvider so an MD5 implementation from MSDN is included. The specifics of formatting the request arguments can be found in the DictionaryExtensions class.

Reading Data

All of the HTTP communication is handled through the WebClient which makes that communication quite easy. For instance all of the Xml retrieval is done with just the following code:

protected override void InvokeMethod(object state)
{
    WebClient client = new WebClient();

    client.DownloadStringCompleted += new DownloadStringCompletedEventHandler(client_DownloadStringCompleted);
    UriBuilder builder = new UriBuilder(RootUri);
    builder.Query = Parameters.ToArgList();

    client.DownloadStringAsync(builder.Uri, state);
}

void client_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
{
    RemoteMethodCallBacks callbacks = (RemoteMethodCallBacks)e.UserState;

    try
    {
        XDocument doc = XDocument.Parse(e.Result);
        callbacks.ReturnSuccess(doc);
    }
    catch (WebException ex)
    {
        callbacks.ReturnError(CreateErrorDocument(ex));
    }
    finally
    {
        ((WebClient)sender).DownloadStringCompleted -= new DownloadStringCompletedEventHandler(client_DownloadStringCompleted);
    }
}

Since all of the responses are Xml the parsing is done right where the data is received. It was a tad surprising to see that the XmlDocument DOM model is not included in WP7 but I'm getting used to XDocument etc.; though I miss XPath. Were you to want to switch to JSON it is here that the code would change. The RemoteMethodCallbacks object holds to Action<XDocument> delegates, one for success and one for failure. These get passed in by the calling code and packaged up in the RemoteMethod base class:

public void Invoke(Action<XDocument> successCallback, Action<XDocument> errorCallback)
{
    InvokeMethod(new RemoteMethodCallBacks(successCallback, errorCallback));
}

Populating the Object Model

So after successfully being able to get some data from the web services I thought I was pretty much home free. A little bit of Xaml UI and wham I'm done. Not so lucky.

See I've always been of the opinion that once you have some Xml you have a Model and I've never see the need to transform Xml into C# objects. For that reason I've always avoided object serialization and deserializations. It just seems like an unnecessary step; especially when you have technologies like WPF binding at your finger tips and can go directly at the Xml. As far as I can tell Linq to Xml data binding is not supported on WP7. C'est la vie. Looks like we'll need object deserialization after all.

Authenticating

But even before we get to doing that we need to authenticate with last.fm and get a session key. Last.fm session keys identify the user and do not expire, so a single log in can last through multiple sessions. For that reason as soon as we successfully get a session key it is saved into IsolatedStorage so the user does not need to log in again. IsolatedStorage is about the only local IO that a WP7 apps has access to.

[DataContract]
public class Session
{
    [DataMember]
    public string SessionKey { get; set; }
    
    public void Authenticate(string username, string md5Password, Action successCallback, Action<XDocument> errorCallback)
    {
        Dictionary<string, string> p = new Dictionary<string, string>();

        p["username"] = username;
        p["authToken"] = MD5Core.GetHashString(username + md5Password);

        RemoteMethod method = new RemoteWriteMethod("auth.getMobileSession", p);
        Debug.Assert(successCallback != null);

        method.Invoke(doc =>
            {
                User.Name = username;
                SessionKey = doc.Descendants("key").First().Value;
                successCallback();
            },
            errorCallback
        );
    }
}

public partial class App : Application
{
    public static void SaveStateToIsolatedStorage()
    {
        using (var applicationStorage = IsolatedStorageFile.GetUserStoreForApplication())
        using (var settings = applicationStorage.OpenFile("settings.xml", FileMode.Create, FileAccess.Write, FileShare.None))
        {
            var document = new XDocument(new XDeclaration("1.0", "utf-8", "yes"),
                new XElement("settings",
                    new XElement("timeStamp", DateTime.Now),
                    new XElement("sk", Session.Current.SessionKey),
                    new XElement("user", Session.Current.User.Name)
                    ));
            document.Save(settings);
        }
    }
}

Hydrating Objects

Once we have a user name and session key we can call any of the other last.fm methods. Within this app the root object is the User which corresponds to the authenticated account. From the User object we load things like the list of their friends, music library, most recent tracks etc. All of these are ObservableCollections and from those collections we can navigate to individual Artists, Albums and other types of interest.

[DataContract(Name="user")]
[InitMethod("user.getInfo")]
public class User : INotifyPropertyChanged, IDisplayable, IShoutable
{
    [DataMember(Name="name")]        
    public string Name { get; set; }
    
    [CollectionBindingAttribute("artist", "user.getRecommendedArtists")]
    [DataMember]        
    public ObservableCollection<Artist> RecommendedArtists { get; set; }    
}

The DataContractAttribute and other attributes from the System.Runtime.Serialization namespace are used for two purposes.

  1. Serializing and deserializing the Model objects into Page State as part of the page lifecycle (more on that later)
  2. Marking types and members with meta data about the last.fm Xml structure that they will be deserialized from during the hydration of Model objects populated from last.fm data

Because of some initial trouble mapping the last.fm Xml structures using System.Xml.Serialization for object deserialization, I rolled my own lightweight implementation rather than using the built in XmlSerializer objects. This allowed me to move forward more quickly and seems to work pretty well without that much code. It uses the DataContract attribute plus some additional ones to help with specific last.fm idioms.

The most interesting of these is the CollectionBindingAttribute which describes the remote method and Xml element names that can be used to populate a specific ObservableCollection.

[CollectionBinding("artist", "user.getRecommendedArtists")]
[DataMember]        
public ObservableCollection<Artist> RecommendedArtists { get; set; }

In the above example the CollectionBinding indicates that the collection is populated by calling the user.getRecommendedArtists method which will return a list of <artist/> Xml elements. The type parameter Artist of the ObservableCollection is used to determine which Model types to instantiate. The code below invokes the remote method asynchronously and when it returns populates the collection with newly instantiated objects.

public class RemoteCollectionLoader<T> where T : new()
{
    public void Load(ICollection<T> collection, Action success, Action<XDocument> fail)
    {
        if (Parameters.ContainsKey("sk") && !string.IsNullOrEmpty(Parameters["sk"]))
        {
            RemoteMethod method = new RemoteReadMethod(MethodName, Parameters);
            method.Invoke(
                d => { OnCollectionLoaded(collection, d); if (success != null) success(); },
                d => { if (fail != null) fail(d); });
        }
    }
    
    private void OnCollectionLoaded(ICollection<T> collection, XDocument data)
    {
        // loop over the collection and create/bind new objects; adding them to the list
        foreach (XElement e in data.Descendants(ElementName))
        {
            T content = new T();
            RemoteObjectFactory.Load(content, e);
    
            if (!collection.Contains(content))
                collection.Add(content);
        }
    }
}

That is the essential approach to populating the entire object model. An object has properties (all of those properties are strings) and ObservableCollections of associated objects. An Artist has a Name and other meta data as well as Shouts, and Albums and similar artists. It is how the app populates the user's Library, Calendar and list of friends and neighbors.

Writing Data

Modifying data on the server involves POSTing a request to the last.fm services. The RemoteWriteMethod does this sending the method parameters as x-www-form-urlencoded again using the WebClient. In this example the Shout method will post a message on the user's profile.

public class User : INotifyPropertyChanged, IDisplayable, IShoutable
{
    public void Shout(string msg)
    {
        var args = new Dictionary<string, string>();
        args["user"] = Name;
        args["message"] = msg;
        args["sk"] = Session.Current.SessionKey; 

        var method = new RemoteWriteMethod("user.shout", args);
        method.Invoke(null, null);
    }
}

internal class RemoteWriteMethod : RemoteMethod
{
    protected override void InvokeMethod(object state)
    {
        WebClient client = new WebClient();
        client.Headers["Content-type"] = "application/x-www-form-urlencoded";

        client.UploadStringCompleted += new UploadStringCompletedEventHandler(client_UploadStringCompleted);

        client.UploadStringAsync(RootUri, "POST", Parameters.ToArgList(), state);
    }
}

The ViewModels

The ViewModels handle the interaction of the UI with the model and as such the ViewModels in this project are no different than other. There is a lot of good information out there on MVVM so I won't go into the detail of what a ViewModel is and how it works. Rather I'll point out a couple points of interest to WP7 and this app.

Handling Navigation Commands

The AppViewModel base class handles requests for navigation coming from the UI and makes a determination whether to open an external browser or not. This is based on if the request Uri is relative or absolute. Absolute addresses are external to the app itself so have to be opened in a WebBrowserTask. The WebBrowserTask is a great way to test how your app handles tombstoning as it puts your app to sleep while the user is using the WebBrowser and wakes it back up when they navigate back. We'll cover a bit more about Tombstoning in the section on the Views.

public abstract class AppViewModel : ViewModelBase
{    protected void Navigate(string address)
    {
        if (string.IsNullOrEmpty(address))
            return;
    
        Uri uri = new Uri(address, UriKind.RelativeOrAbsolute);
        if (uri.IsAbsoluteUri)
        {
            WebBrowserTask browser = new WebBrowserTask();
            browser.URL = address;
            browser.Show();
        }
        else
        {
            Debug.Assert(App.Current.RootVisual is PhoneApplicationFrame);
            ((PhoneApplicationFrame)App.Current.RootVisual).Navigate(uri);
        }
    }
    protected void Navigate(string page, AppViewModel vm)
    {
        string key = vm.GetHashCode().ToString();
        ViewModelLocator.ViewModels[key] = vm;
    
        Navigate(string.Format("{0}?vm={1}", page, key));
    }
}

Navigating Between Views/Pages

The main app ViewModels (profile, library, people and calendar) are bound to their Views by the MVVM ViewModelLocator. This works because those ViewModels are static and available to the entire app. When an item is selected in the UI we need to dynamically create a ViewModel and bind it to a View. This is done by adding it to a static collection of ViewModels and binding it to the View by passing a key to it in the Uri argument list.

public abstract class RemoteObjectViewModel<T> : DisplayableViewModel<T> where T : IDisplayable
{
    protected RemoteObjectViewModel(T item)
        : base(item)
    {
        SelectItemCommand = new RelayCommand(SelectItem);
    }

    protected virtual void SelectItem()
    {
    }

    public RelayCommand SelectItemCommand { get; private set; }
}

public class AlbumViewModel : RemoteObjectViewModel<Album>
{
    protected override void SelectItem()
    {            
        Navigate("/AlbumPage.xaml", this);
    }
}

In the View (a PhoneApplicationPage) we then look in that collection for the ViewModel:

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    if (NavigationContext.QueryString.ContainsKey("vm"))
    {
        string vm = NavigationContext.QueryString["vm"];
        if (ViewModelLocator.ViewModels.ContainsKey(vm))
            Dispatcher.BeginInvoke(() => { DataContext = ViewModelLocator.ViewModels[vm]; });                        
    }

    base.OnNavigatedTo(e);
}

When the page is navigated away from in the Back direction it is no longer reachable to so we can remove the ViewModel form the collection and clean it up:

protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
    base.OnNavigatingFrom(e);

    AppViewModel vm = DataContext as AppViewModel;
    if (vm != null)
    {       
        string key = vm.GetHashCode().ToString();
        if (e.NavigationMode == NavigationMode.Back && ViewModelLocator.ViewModels.ContainsKey(key))
        {
            ViewModelLocator.ViewModels[key].Cleanup();
            ViewModelLocator.ViewModels.Remove(key);                    
        }
    }
}

Dealing with Asynchrony

Of course one the most challenging parts of a web client app is dealing with asynchronous data transfer. Most of the async calls in this app are spent querying last.fm for collections of objects and filling up ObservableCollections. Most of that heavy lifting is handled by the RemoteCollectionLoader shown above. Some of that work, specifically initiating the asynchronous communication, is the responsibility of the ViewModels.

In this example, the ProfileViewModel (which sits behind the main Profile View) exposes a RecommendArtists property. When this property is invoked by the View during binding the an ItemsSourceViewModel is retrieved from a collection. Once retrieved it is told to load its contents asynchronously and then returned to the caller. Once the data is retrieved and the collection is populated with Model objects a new ViewModel object is created for each item in the collection. The View binds to the ViewModelItems property which contains the resulting set of ViewModels.

public class ProfileViewModel : DisplayableViewModel<User>
{       
public ProfileViewModel(User u)
        : base(u)
    {
        ViewModels.Args["user"] = Item.Name;
        ViewModels.Add<Artist>(new Item>sSourceViewModel<User, Artist>("RecommendedArtists", item => new ArtistViewModel(item)));         
    }
    
    public AppViewModel RecommendedArtists
    {
        get
        {
            return ViewModels.GetViewModel<Artist>("RecommendedArtists", Item.RecommendedArtists);
        }
    }
}

public class ViewModelCollection<TParent> : INotifyPropertyChanged
{
    private Dictionary<string, AppViewModel> _viewModels = new Dictionary<string, AppViewModel>(StringComparer.Ordinal);
    public ItemsSourceViewModel<TParent, TItem> GetViewModel<TItem>(string name, ICollection<TItem> collection) where TItem : new()
    {
        if (_viewModels.ContainsKey(name))
        {
            var vm = (ItemsSourceViewModel<TParent, TItem>)_viewModels[name];
            vm.Load(Args, collection);

            return vm;
        }

        return null;
    }
}

public class ItemsSourceViewModel<TParent, TItem> : AppViewModel where TItem : new()
{
    public void Load(IDictionary<string, string> args, ICollection<TItem> items)
    {
        _items = items;
        _args = args;

        if (items.Count < 1)
        {
            Working = true;
            var loader = RemoteObjectFactory.CreateLoader<TItem>(typeof(TParent), Name, args);
            loader.Parameters["sk"] = Session.Current.SessionKey;

            loader.Load(items, LoadComplete, SetLastError);
        }
        else
        {
            LoadComplete();
        }
    }

    protected void LoadComplete()
    {
        ViewModelItems = _items.Select(item => factory(item));

        Working = false;
    }

    private IEnumerable _viewModelItems;
    public IEnumerable ViewModelItems
    {
        get { return _viewModelItems; }
        protected set
        {
            _viewModelItems = value;
            RaisePropertyChanged("ViewModelItems");
        }
    }
}

The Views

The codebehind base class

All of the pages in the project inherit from LastFmPage which provides some basic shared functionality like dealing with changes to Authentication state.

Page lifecycle

When a page is navigated away from WP7 puts it to sleep. When the user navigates back to a page it is deserialized and reinstated. A page that is reawoken should rebuild itself so that it appears the same as when the user left it. Managing and restoring page state is helped by the base class. Since in many cases the page is bound to a ViewModel determined by user interaction and not statically attached the ViewModelLocator the page may need to store some state about what is being displayed. WP7 provides a State dictionary for this purpose. In order to be storable in the State dictionary the object must be serializable. Rather than serialize the ViewModel instance, I store the Model object (since those are already completely serializable and the type of ViewModel so that it can be instantiated later.

protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
    base.OnNavigatingFrom(e);

    AppViewModel vm = DataContext as AppViewModel;
    if (vm != null)
    {
        object model = vm.GetModel();
        if (model != null)
        {
            State["dctype"] = vm.GetType().AssemblyQualifiedName;
            State["model"] = model;
            State["stamp"] = DateTime.Now;
        }
    }
}

Then when the page is rehydrated it can retrieve the Model from the state dictionary, create a new instance of the correct ViewModel and get itself wired back up.

protected bool IsResurrectedPage
{
    get
    {
        return _newPageInstance && this.State.ContainsKey("PreservingPageState");
    }
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
    if (IsResurrectedPage && State.ContainsKey("model") && State.ContainsKey("dctype"))
    {
        object model = State["model"];
        Type t = Type.GetType(State["dctype"].ToString(), false);

        if (t != null && model != null)
        {
            AppViewModel vm = Activator.CreateInstance(t, model) as AppViewModel;
            if (vm != null)
            {
                if (State.ContainsKey("stamp"))
                {
                    // if the data is more than an hour old refresh it
                    DateTime stamp = (DateTime)State["stamp"];
                    if (DateTime.Now - stamp gt; new TimeSpan(1, 0, 0))
                        vm.Refresh();
                }
                Dispatcher.BeginInvoke(() => { DataContext = vm; });
            }
        }
    }

    base.OnNavigatedTo(e);
}
ApplicationBar event handling

The ApplicationBar is a bit of a strange beast that cannot be data bound to the view model. For this reason the page base class handles the app bar events. This uses the IApplicationBarMenuItem text property as a command identifier and reacts appropriately. It isn't the most robust system in the world but it works and allows pages to share App Bar functionality with a single event handler. Hopefully someone will create a mechanism to allow Application Bar command binding.

// in a LastFmPage derived class
private void ApplicationBar_Click(object sender, EventArgs e)
{
    AppBarButtonPressed(((IApplicationBarMenuItem)sender).Text);
}

// in the LastFmPage class
protected virtual void AppBarButtonPressed(string text)
{
    if (text == "logout")
    {
        AppViewModel vm = DataContext as AppViewModel;
        if (vm != null && vm.SignOutCommand != null && vm.SignOutCommand.CanExecute(null))
            vm.SignOutCommand.Execute(null);
    }
    else if (text == "refresh")
    {
        AppViewModel vm = DataContext as AppViewModel;
        if (vm != null)
            vm.Refresh();
    }
    else if (text == "profile")
    {
        NavigationService.Navigate(new Uri("/HomePage.xaml", UriKind.Relative));
    }
    else if (text == "library")
    {
        NavigationService.Navigate(new Uri("/LibraryPage.xaml", UriKind.Relative));
    }
    else if (text == "events")
    {
        NavigationService.Navigate(new Uri("/CalendarPage.xaml", UriKind.Relative));
    }
    else if (text == "people")
    {
        NavigationService.Navigate(new Uri("/NeighboursPage.xaml", UriKind.Relative));
    }
    else if (text == "search")
    {
        NavigationService.Navigate(new Uri("/SearchPage.xaml", UriKind.Relative));
    }
    else
    {
        AppViewModel vm = DataContext as AppViewModel;
        if (vm != null && vm.Commands.ContainsKey(text))
        {
            var command = vm.Commands[text];
            command.Execute(null);
        }
    }
}

TitlePanel

Every page includes a UserControl which presents a consistent visual header as well as a mechanism to present an error message and show a progress indicator

<local:TitlePanelControl Grid.Row="0" VerticalAlignment="Top"/> 
(both the Error and Working properties existing on the AppViewModel base class).
<TextBlock Foreground="Red" TextWrapping="Wrap"
    Text="{Binding Error}"/>

<ProgressBar 
    IsIndeterminate="{Binding Working}" 
    Visibility="{Binding Working, Converter={StaticResource VisibilityConverter}}"
    Style="{StaticResource CustomIndeterminateProgressBar}"/>

Tombstoning

Like each page, the entire application can be put to sleep. This happens when the user navigates to another app, perhaps when a WebBrowserTask is shown or by hitting the home button. This is referred to as Tombstoning and the app is expected to store and retreive state so that if it gets navigated back to it looks to the user as if it never went away.

Mostly this involves reacting to four static events on the Application:

  1. Launching - when the app is cold started. The docs say not to access IsolatedStorage from this event as it will slow the launch. I kick off a background thread to do that in this event handler so that the app starts nice and quickly and then populates itself after it is shown
  2. Activated - - when the apps is navigated back to after being tombstoned. The PhoneApplcationService provides a state dictionary for the app to retrieve state from that can be used here
  3. Deactivated - the app is being tombstoned. Save state to the PhoneApplcationService and to IsolatedStorage (because this logical instance of the app may not ever be untombstoned)
  4. Closing - when the app falls off the navigation stack such that it can be navigated back to any longer. Save state to IsolatedStorage here
// Code to execute when the application is launching (eg, from Start)
// This code will not execute when the application is reactivated
private void Application_Launching(object sender, LaunchingEventArgs e)
{
    RootFrame.Dispatcher.BeginInvoke(LoadStateFromIsolatedStorage);
}

// Code to execute when the application is activated (brought to foreground)
// This code will not execute when the application is first launched
private void Application_Activated(object sender, ActivatedEventArgs e)
{
    LoadStateFromService();
}

// Code to execute when the application is deactivated (sent to background)
// This code will not execute when the application is closing
private void Application_Deactivated(object sender, DeactivatedEventArgs e)
{
    SaveStateToService();

    SaveStateToIsolatedStorage();
}

// Code to execute when the application is closing (eg, user hit Back)
// This code will not execute when the application is deactivated
private void Application_Closing(object sender, ClosingEventArgs e)
{
    SaveStateToIsolatedStorage();       
}

The UI

The root of the UI is four pages that display the user's profile, people, library and events. Each of these pages has an entry on the ApplicationBar of each of the other main pages. Each page consists of a Pivot control that breaks the page into lists of sub items that can be navigated to. Each subitem (such as an artist, event or person) itself has a page with a Pivot control that breaks that item down into more detail and allows the user to drill down and across the content of their last.fm account.

So most of the UI is made up of Pivot controls. When they are displaying a grid of images those are clickable buttons displayed in a WrapPanel.

<ItemsPanelTemplate x:Key="PivotItemPanelTemplate">
    <toolkit:WrapPanel  ItemHeight="228" ItemWidth="228"/>
</ItemsPanelTemplate>
<DataTemplate x:Key="PivotItemDataTemplate">
    <Button Style="{StaticResource ImageButtonStyle}" CacheMode="BitmapCache" 
            cmd:ButtonBaseExtensions.Command="{Binding SelectItemCommand}">
        <StackPanel Background="Transparent" >
            <Image Stretch="UniformToFill" Width="220" Height="220"                     
               Source="{Binding LargeImage}"/>
            <Grid Background="Black" Margin="0,-20,0,0" Opacity="0.5" 
                  Height="20"/>
            <TextBlock Margin="0,-25,0,0" Width="218" Foreground="White"  Text="{Binding Name}" FontSize="{StaticResource PhoneFontSizeNormal}">                              
                <TextBlock.Clip>
                   <RectangleGeometry Rect="0,0,218,150"/>  
                </TextBlock.Clip>
            </TextBlock>
        </StackPanel>
    </Button>
</DataTemplate>

The ItemsSource for the WrapPanels, ListBoxes and ItemsControls used in the UI get a DataContext which is a ItemsSourceViewModel (described above). In turn their ItemsSource property gets bound to the ViewModelItems collection on the ItemsSourceViewModel. In this way as the user navigates through the app they are navigating through the view models and things just kind of wire themselves up as they go along.

<controls:PivotItem Header="artists" DataContext="{Binding RecommendedArtists}">
    <ScrollViewer x:Name="RecommendedArtistsScrollViewer">
        <ItemsControl 
        ItemsSource="{Binding Path=ViewModelItems}"
        ItemsPanel="{StaticResource PivotItemPanelTemplate}" 
        ItemTemplate="{StaticResource PivotItemDataTemplate}"/>
    </ScrollViewer>
</controls:PivotItem>

Miscellaneous

Command binding

MVVM Light's command binding is invaluable linking up the UI to ViewModel ICommands:

 <HyperlinkButton Content="website"
    cmd:ButtonBaseExtensions.Command="{Binding Path=NavigateCommand}"
    cmd:ButtonBaseExtensions.CommandParameter="{Binding Item.Website}"
    Visibility="{Binding HasWebsite, Converter={StaticResource VisibilityConverter}}"/>.
Indeterminate progress bar

Every Page has a <ProgressBar IsIndeterminate="{Binding Working}"/> at the top. The Working property exists on the AppViewModel base class so in order to inform the UI that something is going on, all a view model has to do is set that to true and the nice built in progress indicator will show up.

The problem with the current version of the control is that it animates on the UI thread. So if your UI is doing something in addition to displaying progress it can be kind of jerky. MSDN has a code snippet that moves the animation to the compositor thread and I'd recommend using this approach as it has a noticeable improvement.

Page Transitions

There isn't much built in support for animated page transitions but there are a couple of alternatives for including them. The Silverlight Toolkit now has a page transition solution. I went with some code form Clarity consulting which works pretty nicely. They really seem to know their stuff when it comes to WP7 and it is as easy to include as having my page base class inherit form theirs and then set an AnimationContext property in my page constructors. The AnimationContext determines what visual element will animate when moving from page to page. If left unset it animates the entire page. I decided to set it to the page content so that the header area (the last.fm logo) appears as if it is static between pages.

public partial class EventPage : LastFmPage
{
    public EventPage()
    {
        InitializeComponent();
        AnimationContext = ContentPanel;
    }
}
Button TiltEffect

Buttons in WP7 apps have a tilt behavior, where they depress at the point that they are tapped. This doesn't come built in but there is code on MSDN to create that same behavior. It very easy to use and all you need to do is turn it on statically as the app starts and it applies to any buttons your UI creates.

public partial class App : Application
{
    private void Application_Launching(object sender, LaunchingEventArgs e)
    {
        TiltEffect.SetIsTiltEnabled(RootFrame, true);
    }

    // Code to execute when the application is activated (brought to foreground)
    // This code will not execute when the application is first launched
    private void Application_Activated(object sender, ActivatedEventArgs e)
    {           
        TiltEffect.SetIsTiltEnabled(RootFrame, true);
    }
}

Submitting the App

Submitting the app to the the app hub was very easy. Microsoft's site walks you through the process and honestly the hardest thing was just the tedium of capturing and sizing the artwork and screen shots.

The very first submission got rejected based on how it looked one the WP7 Light theme (as in it did not look good). I hadn't even considered the light theme prior to submission so do learn form my mistake.

  • Don't set the ApplicationBar colors unless you really want those colors on both themes.
    You can't DataBind them and they will be switched automatically per Theme by WP7, but not if you have set the colors by hand in your XAML.
  • Don't use color icons on the ApplicationBar
    Again, they may look great on the dark theme but switch to the Light theme and they will be drawn as black outlines at best, black blobs at worst. Use White on Transparent icons. They look fine on both themes. There are some good ones online. Don't include the outer circle (the app bar adds it) and set the image itself to 24x24.

Points of Interest

I had to tackle a number of things that were new to me while writing this app: REST, Silverlight, the WP7 platform. Next time around I'll do a few of things differently:

  • Find a pre-built REST client (i.e. something built in or a third party framework)
  • Use JSON and built in object deserialization

For both of those I rolled my own more out of frustration with getting something/anything working and a desire to move on to other parts of the app than anything else. My philosophy on things like that is "the best code is already written and tested by somebody else" and I always hesitate to do myself something that I'm sure someone else has already done.

  • Explore Reactive Extensions. I'm pretty sure the asynchronous stuff could be better abstracted using Rx and that will be an interesting area for investigation on the next app (whatever that turns out to be)

Oh and don't put a MapControl on a Pivot control. It's confusing and doesn't work.

History

  • 12/5/2010 - Initial upload

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Don Kackman
Team Leader Starkey Laboratories
United States United States
The first computer program I ever wrote was in BASIC on a TRS-80 Model I and it looked something like:
10 PRINT "Don is cool"
20 GOTO 10
It only went downhill from there.
 
Hey look, I've got a blog
Follow on   Twitter

Comments and Discussions

 
GeneralMy vote of 5 PinmemberWeidong Shen7-Jan-11 9:54 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.141223.1 | Last Updated 21 Dec 2010
Article Copyright 2010 by Don Kackman
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid