Introduction
After swearing of Windows phones a couple of years back or so, I am back in the possession of a 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 OSs 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 <a href="http://msdn.microsoft.com/en-us/library/system.net.webclient%2528VS.95%2529.aspx">WebClient</a>
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 seen 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 Artist
s, Album
s 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.
- Serializing and deserializing the Model objects into Page State as part of the page lifecycle (more on that later)
- 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)
{
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 string
s) and ObservableCollection
s of associated objects. An Artist
has a Name
and other meta data as well as Shout
s, and Album
s 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 <a href="http://www.last.fm/api/show?service=411">Shout</a>
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 <a href="http://msdn.microsoft.com/en-us/library/ff769550%2528v=VS.92%2529.aspx">WebBrowserTask</a>
. 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 so we can remove the ViewModel
from 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 ViewModel
s.
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, 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 ViewModel
s.
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"))
{
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.
private void ApplicationBar_Click(object sender, EventArgs e)
{
AppBarButtonPressed(((IApplicationBarMenuItem)sender).Text);
}
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:
- 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 - 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 - 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) - Closing - when the app falls off the navigation stack such that it can be navigated back to any longer. Save state to
IsolatedStorage
here
private void Application_Launching(object sender, LaunchingEventArgs e)
{
RootFrame.Dispatcher.BeginInvoke(LoadStateFromIsolatedStorage);
}
private void Application_Activated(object sender, ActivatedEventArgs e)
{
LoadStateFromService();
}
private void Application_Deactivated(object sender, DeactivatedEventArgs e)
{
SaveStateToService();
SaveStateToIsolatedStorage();
}
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 WrapPanel
s, ListBox
es and ItemsControl
s 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 ICommand
s:
<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);
}
private void Application_Activated(object sender, ActivatedEventArgs e)
{
TiltEffect.SetIsTiltEnabled(RootFrame, true);
}
}
Submitting the App
Submitting the app to 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