Building a detailed About page for your Windows Phone application
July 7, 2011
I’ve gotten enough questions about the “About” page in my app that I figure it’s time to share it.
Is this the best way to do it? No clue… last year when I started building my first app, this is how I decided to write the about page in a few unique ways, so here it is!
The tech industry has a long and fun history, having gone many directions, with “credits”, about experiences, and more. Do you remember when Internet Explorer had a Credits listing? Photoshop always starts up with Thomas Knoll and friends.
With mobile apps, the quickest way to make an app that rocks is to borrow most of the app. That’s where helper libraries, toolkits, open source friends, and everything else comes together into one shippable piece of marketplace goo.
Now, with most open source projects, you definitely need to remember to keep the license files in tact, track your sources, make sure licenses are compatible… and whatever else your lawyer tells you.
But it’s also a pretty common practice to give public credit to the developers who helped make your app possible, and as a developer, I’ll admit my ego doesn’t mind seeing my name plastered in an about page on a favorite app or two.
What I put into my About page
I decided to put a lot of good information in the about experience, separated using a Pivot control and a few items. At one point and time, the marketplace ingestion requirements asked for support data and some other things for all apps, but I believe these requirements have been relaxed. So, I have:
- Main pivot item and about screen
- App name, author, version dynamically populate from the app package
- A link to review the app
- Support information
- Links to the app’s privacy policy
- Legal page with lots of text
- “What’s new” dynamic pivot item
Note: A nice ‘About’ experience can have zero performance impact
The About page for my app is contained in its own assembly, “About.dll”. It’s small in size, but since it’s a separate assembly not often used (and not loaded at startup).
According to analytics data, less than 3% of the time people look at my About page, so most probably have not seen it – why make them pay for it?
All that’s required for this is:
- Referencing the About project/assembly from your main app’s project
- Navigating to this URI: /About;component/About.xaml
Creating the info page
So to get started…
- Add a new class library project to your app’s solution; I named mine simply, ‘About’
- Delete the Class1.cs that comes with it
- Add an About.xaml to the project
- Add any XMLNS and project references you may need (such as the TiltEffect from the toolkit, etc.)
In the pivot, I set its Title to my app’s name, then put in a few text blocks and hyperlink buttons. I’ve named the version block and also have a Review this app button.
<controls:PivotItem Header="about"> <ScrollViewer> <StackPanel Margin="0,-12,0,24"> <TextBlock Style="{StaticResource PhoneTextExtraLargeStyle}" Text="4th & Mayor" Foreground="{StaticResource PhoneAccentBrush}" /> <TextBlock Style="{StaticResource PhoneTextLargeStyle}" Text="by Jeff Wilcox" /> <HyperlinkButton NavigateUri="http://www.4thandmayor.com/" TargetName="_new" HorizontalAlignment="Left" Content="www.4thandmayor.com" /> <StackPanel Orientation="Horizontal" Margin="0,18,0,0"> <TextBlock Style="{StaticResource PhoneTextNormalStyle}" Text="Version:" /> <TextBlock Margin="0" Style="{StaticResource PhoneTextNormalStyle}" x:Name="_versionText" /> </StackPanel> <Button HorizontalAlignment="Left" Tag="Review" Click="HyperlinkButton_Click" Content="Review this app"/> </StackPanel> </ScrollViewer> </controls:PivotItem>
In code behind, I’m then hooking up a few quick things…
private void HyperlinkButton_Click(object sender, RoutedEventArgs e) { string s = ((ButtonBase)sender).Tag as string; switch (s) { case "Review": var task = new MarketplaceReviewTask(); task.Show(); break; } }
And the version that I pull in isn’t pretty code, but it’ll grab it from the manifest file. This can be done in the constructor or in the OnNavigatedTo event:
Uri manifest = new Uri("WMAppManifest.xml", UriKind.Relative); var si = Application.GetResourceStream(manifest); if (si != null) { using (StreamReader sr = new StreamReader(si.Stream)) { bool haveApp = false; while (!sr.EndOfStream) { string line = sr.ReadLine(); if (!haveApp) { int i = line.IndexOf("AppPlatformVersion=\"", StringComparison.InvariantCulture); if (i >= 0) { haveApp = true; line = line.Substring(i + 20); int z = line.IndexOf("\""); if (z >= 0) { // if you're interested in the app plat version at all // AppPlatformVersion = line.Substring(0, z); } } } int y = line.IndexOf("Version=\"", StringComparison.InvariantCulture); if (y >= 0) { int z = line.IndexOf("\"", y + 9, StringComparison.InvariantCulture); if (z >= 0) { // We have the version, no need to read on. _versionText.Text = line.Substring(y + 9, z - y - 9); break; } } } } } else { _versionText.Text = "Unknown"; }
OK, so that gives us at least a simple starting point for the about page. Of course a solid app would also try and tombstone the select pivot item, etc.
My dynamic LICENSE.TXT solution
Instead of just embedding the XAML for my ‘legal’ pivot item, I decided that it would be much easier to maintain if I just pulled this information in from a file inside the application’s installation directory called LICENSE.TXT. As an app dev, I’m used to using this kind of file to track dependencies and credit.
Prepare the file:
- In your main application’s project, right-click in VS and add a new Text File
- Select the file, open the Properties window
- Change the type of the file to ‘Content’
Content will make sure that the file ends up inside your XAP (which is ‘exploded’ on the client into an installation directory), but it won’t take up space in any of the app’s assemblies.
Prep the pivot and pivot item:
First, let’s add the new pivot item for the ‘legal’ information.
<controls:PivotItem Header="legal"> <ScrollViewer x:Name="sv1" Margin="0,0,-12,24"/> </controls:PivotItem>
Pretty simple. Note I named the scroll viewer “sv1” so that I could dynamically add everything I needed to it. This is partly to workaround the 2000x2000 pixel limitation for large things like text blocks, so that it doesn’t clip.
Add a SelectionChanged event to the pivot and wire it up (or just do it in XAML). I use this to wait until someone swipes to the ‘legal’ pivot item to actually load in the file. It’s all such little work for the device to do that you really could just do it in the page constructor, too, but when I first coded this, I decided to do it this way!
This is probably way too code heavy, but hey it works. It uses the app’s GetResourceStream to load the LICENSE.txt from the installation directory. I also decided that when the code encounters a blank line, that the next will have full opacity (instead of 0.7), so it stands out. This helps designate ‘sections’ in the text.
private void Pivot_SelectionChanged(object sender, SelectionChangedEventArgs e) { Pivot piv = (Pivot)sender; if (piv.SelectedIndex > 0 && _licenses == null) { Dispatcher.BeginInvoke(() => { _licenses = new StackPanel(); StreamResourceInfo sri = Application.GetResourceStream( new Uri("LICENSE.txt", UriKind.Relative)); if (sri != null) { using (StreamReader sr = new StreamReader(sri.Stream)) { string line; bool lastWasEmpty = true; do { line = sr.ReadLine(); if (line == string.Empty) { Rectangle r = new Rectangle { Height = 20, }; _licenses.Children.Add(r); lastWasEmpty = true; } else { TextBlock tb = new TextBlock { TextWrapping = TextWrapping.Wrap, Text = line, Style = (Style)Application.Current.Resources["PhoneTextNormalStyle"], }; if (!lastWasEmpty) { tb.Opacity = 0.7; } lastWasEmpty = false; _licenses.Children.Add(tb); } } while (line != null); } } sv1.Content = _licenses; }); } }
A web-connected “What’s New” display
OK, so now we have a static first page, a runtime-read license display, and next, let’s finish off with a dynamic, web-requested block of XAML.
When I created the app, I really wanted to be able to provide updated information or bug listings on the fly if needed. I decided to host a XAML file in the cloud; if the file can be resolved, it’s downloaded and displayed, and if not, it falls back to some basic text.
Another idea I had, but never implemented fully, was to have a blog/RSS feed with ‘known issues’, and then let the app pick up the recent posts and show them inside this tab: then users could see if they’re experiencing issues, and if they were, could even go to the blog post and comment on it with others. Didn’t get there. Maybe in a future update I’ll spend the time!
So here’s what my final pivot item looks like in XAML
<controls:PivotItem Header="what's new?"> <ScrollViewer> <shared:WebXamlBlock Margin="0,24,12,24" VerticalAlignment="Top" HorizontalContentAlignment="Left" XamlUri="http://www.4thandmayor.com/app/changelog.xaml"> <shared:WebXamlBlock.FallbackContent> <StackPanel> <TextBlock TextWrapping="Wrap" Style="{StaticResource PhoneTextLargeStyle}">Information about the latest version can be found out at:</TextBlock> <TextBlock Text=" " /> <HyperlinkButton HorizontalAlignment="Left" Style="{StaticResource AccentHyperlink}" FontSize="{StaticResource PhoneFontSizeMediumLarge}" NavigateUri="http://www.4thandmayor.com/" Content="http://www.4thandmayor.com/" TargetName="_self" /> </StackPanel> </shared:WebXamlBlock.FallbackContent> </shared:WebXamlBlock> </ScrollViewer> </controls:PivotItem>
So the final piece that’s missing is the ‘Shared:WebXamlBlock’ control that dynamically downloads from the web the XAML. If you use this, be sure to have a complete security review and make sure it’s OK for you to grab dynamic pieces, some people might be spooked by that.
Here is a version of the control; I’ve removed a few dependencies I had on other components I had build to support my projects. The control is a ContentControl and has no default style so it’s simple to get going.
using System.Windows.Controls; using System; using System.Windows; using System.Net; using System.Windows.Data; using System.Windows.Markup; namespace JeffWilcox.Controls { public class WebXamlBlock : ContentControl { #region public Uri XamlUri /// <summary> /// Gets or sets the XAML Uri. /// </summary> public Uri XamlUri { get { return GetValue(XamlUriProperty) as Uri; } set { SetValue(XamlUriProperty, value); } } /// <summary> /// Identifies the XamlUri dependency property. /// </summary> public static readonly DependencyProperty XamlUriProperty = DependencyProperty.Register( "XamlUri", typeof(Uri), typeof(WebXamlBlock), new PropertyMetadata(null, OnXamlUriPropertyChanged)); /// <summary> /// XamlUriProperty property changed handler. /// </summary> /// <param name="d">WebXamlBlock that changed its XamlUri.</param> /// <param name="e">Event arguments.</param> private static void OnXamlUriPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { WebXamlBlock source = d as WebXamlBlock; source.TryDownloading(); } #endregion public Uri XamlUri #region public object FallbackContent /// <summary> /// Gets or sets the content to fallback to if the request fails. /// </summary> public object FallbackContent { get { return GetValue(FallbackContentProperty) as object; } set { SetValue(FallbackContentProperty, value); } } /// <summary> /// Identifies the FallbackContent dependency property. /// </summary> public static readonly DependencyProperty FallbackContentProperty = DependencyProperty.Register( "FallbackContent", typeof(object), typeof(WebXamlBlock), new PropertyMetadata(null)); #endregion public object FallbackContent public WebXamlBlock() { } public override void OnApplyTemplate() { base.OnApplyTemplate(); TryDownloading(); } private bool _haveTried; private void TryDownloading() { if (_haveTried) { return; } _haveTried = true; if (XamlUri != null) { var wc = new WebClient(); wc.DownloadStringCompleted += OnDownloadStringCompleted; wc.DownloadStringAsync(XamlUri); } } private void OnError() { Dispatcher.BeginInvoke(() => { var b = new Binding("FallbackContent") {Source = this}; SetBinding(ContentProperty, b); }); } private void OnDownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e) { if (e.Error != null || e.Cancelled) { OnError(); } else { string xaml = e.Result; Dispatcher.BeginInvoke(() => { try { var o = XamlReader.Load(xaml); if (o != null) { Content = o; } } catch { OnError(); } }); } } } }
And now, for your actual XAML file that you host in the cloud, just drop your XAML in the file with the appropriate standard XML namespaces for XAML and you’re good-to-go.
Fun aside
Activity if you’re ever super bored:
- borrow a friends’ iOS device.
- Go into the Settings.
- Touch ‘General’.
- Touch ‘About’.
- Touch ‘Legal’.
- Begin reading and/or scrolling
That’s right, it’s just like the iTunes user agreement! But it’s also full of information about the tons of libraries and contributors that make that product.
Source note
My implementation is actually pretty tightly coupled to a bunch of random components; until I update the source drop of my app and framework online, this post’s walkthrough will have to do – I don’t have a separate .csproj file with the About app right now. Sorry!