20 January 2011

A Standard About Page for Windows Phone 7 applications

Update - if your read this, you might want to read this update too when you are done

I admit – I fell for it too. I was a bit too lax reading the Windows Phone 7 Application Certification Requirements and missed section 5.6:

An application must include the application name, version information, and technical support contact information that are easily discoverable.”

Like most, I got away with it the first three times since Microsoft initially did not enforce it very strictly - because the rule came in place very late in the process or something like that. But I fell flat on my face the fourth time. I spotted a very nice about page on the Slim Tanken App by Manfred Dalmeijer, fellow member of the Dutch Windows Phone 7 developer community. He was kind enough to send me a sample solution from which I happily copied a lot of XAML. I decided to not only include it in my own Map Mania App, but to build a kind of standard page and MVVMLight model around it – so that everyone who wanted to quickly add a functional About page with support info, review and buy links for his or her App could easily do so. So here goes, the Standard About Page:

I started out the usual way:

  • Create a new Windows Phone application – this one I called “StandardAboutPage”
  • Add a Solution Folder “Binaries”
  • Put “GalaSoft.MvvmLight.WP7.dll”, “GalaSoft.MvvmLight.Extras.WP7.dll” and “System.Windows.Interactivity.dll” in it.

Repeat after me, kids: ” Thou shalt not make unto thee any Windows Phone 7 application without MVVMLight”

Then I added a new Windows Phone 7 class library that I called “LocalJoost.Utilities” – but feel free to pick your own name ;-) . I added references to “Microsoft.Phone.dll” to this library, and thus the library was ready for the AboutViewModelBase class. This class has the following string properties, which I will not write out for the sake of brevity:

  • AppTitle
  • About
  • Buy
  • BuyTheApp
  • CompanyUrl
  • Copyright
  • Review
  • ReviewTheApp
  • Support
  • SupportEmail
  • SupportMessage

There are also three properties that retrieve information from the application itself:

private bool? _trialMode;
public bool IsTrialMode
{
  get
  {
    if (_trialMode == null)
    {
      var s = new LicenseInformation();
#if DEBUG
      _trialMode = true;
#else
      _trialMode = new bool?(s.IsTrial());
#endif
    }
    return _trialMode.Value;
  }
}

public Visibility BuyPanelVisible
{
  get { return IsTrialMode || DesignerProperties.IsInDesignTool ? 
    Visibility.Visible : Visibility.Collapsed; }
}

public string ApplicationVersion
{
  get
  {
    if (DesignerProperties.IsInDesignTool)
      return "version x.x.x";

    Assembly assembly = Assembly.GetExecutingAssembly();
    var name = new AssemblyName(assembly.FullName);
    return name.Version.ToString(3);
  }
}

The rest of the class looks like this:

using System.ComponentModel;
using System.Reflection;
using System.Windows;
using System.Windows.Input;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using Microsoft.Phone.Marketplace;
using Microsoft.Phone.Tasks;

namespace LocalJoost.Utilities
{
  public class AboutViewModelBase : ViewModelBase
  {

    public ICommand BuyCommand
    {
      get
      {
        return new RelayCommand(() => 
          new MarketplaceDetailTask().Show());
      }
    }

    public ICommand ReviewCommand
    {
      get
      {
        return new RelayCommand(() => 
          new MarketplaceReviewTask().Show());
      }
    }

    public ICommand ViewWebsiteCommand
    {
      get
      {
        return new RelayCommand(() =>  
          new WebBrowserTask { URL = CompanyUrl }.Show());
      }
    }

    public ICommand SupportQuestionCommand
    {
      get
      {
        return new RelayCommand(() =>
          {
            var emailComposeTask = new EmailComposeTask
            {
              To = SupportEmail,
              Subject =
                Support + " " + AppTitle + " " +
                ApplicationVersion
            };
            emailComposeTask.Show();
          });
      }
    }

    public void LoadValuesFromResource<T>()
    {
      var targetType = GetType();
      var sourceType = typeof(T);
      foreach (var targetProperty in targetType.GetProperties())
      {
        var sourceProperty =  sourceType.GetProperty(targetProperty.Name, 
                      BindingFlags.Static | BindingFlags.Public);
        if (sourceProperty != null)
        {
          targetProperty.SetValue(this, 
             sourceProperty.GetValue(null, null), null);
        }
      }
    }
  }
}

These are the four commands corresponding to the four basic tasks the user accessing the about page should be able to perform:

  • buy the App (if the current version is a trial version)
  • review the App
  • view the support website
  • send a support question e-mail

The LoadValuesFromResource<T> method is a typical implementation of the paradigm that every good programmer is a lazy programmer –  if you make sure your resource key names match the AboutViewModelBase property names, this method will automatically copy values from the resource to the AboutViewModel class. This will be demonstrated later on.

Anyway, in my StandardAboutPage project I made a Windows Phone Portrait Page “AboutPage” and a resource file “AppResources” with the following entries:

resources

I always go to the resource file's properties and set "Custom Tool" to "PublicResXFileCodeGenerator". This is not the default. Anyway, in that same project I make a very simple class AboutViewModel:

using LocalJoost.Utilities;

namespace StandardAboutPage
{
  public class AboutViewModel : AboutViewModelBase
  {
    public AboutViewModel()
    {
      LoadValuesFromResource<AppResources>();
    }
  }
}

In the constructor I simply call the LoadValuesFromResource method from the base class, pass my resource type and the ViewModel is ready and filled. In order to make this work, your StandardAboutPage project needs to have references to three dll’s in the “Binaries” solution folder al well as to the “LocalJoost.Utilities” project.

All that’s left now is a ‘bit’ of XAML. First of all, you add the following namespaces to your AboutPage.xaml:

xmlns:LJUtilities="clr-namespace:StandardAboutPage"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:GalaSoft_MvvmLight_Command="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.WP7"

Be aware that in this blog text between double quotes is split over two lines due to space limitations, but in your code they should be on one lines. Then, just above the “LayoutRoot” grid, you add the following resource

<phone:PhoneApplicationPage.Resources>
  <LJUtilities:AboutViewModel x:Key="AboutViewModel"/>
</phone:PhoneApplicationPage.Resources>
And then basically you are free to make up your XAML. I replaced the whole default LayoutRoot grid by what I got form Manfred, which I basically only adapted to use the ViewModel's properties and commands:
<Grid x:Name="LayoutRoot" DataContext="{StaticResource AboutViewModel}">
  <Grid.RowDefinitions>
    <RowDefinition Height="Auto" />
    <RowDefinition Height="*" />
  </Grid.RowDefinitions>

  <!--TitlePanel contains the name of the application and page title-->
  <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,12">
    <TextBlock x:Name="ApplicationTitle" Text="{Binding AppTitle}" 
      Style="{StaticResource PhoneTextNormalStyle}" />
    <Grid>
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="60" />
        <ColumnDefinition />
      </Grid.ColumnDefinitions>
      <Canvas xmlns="http://schemas.microsoft.com/client/2007" 
     Width="15.583" Height="60.000">
        <Path Fill="{StaticResource PhoneAccentBrush}" Data=" M 15.393,3.596 C 17.718,19.307 -2.038,16.232 0.173,5.387 C 1.435,-0.804 11.028,-2.002 15.393,3.596 Z" />
        <Path Fill="{StaticResource PhoneAccentBrush}" Data=" M 1.963,18.816 C 5.843,18.816 9.722,18.816 13.602,18.816 C 13.602,32.545 13.602,46.272 13.602,60.000 C 9.722,60.000 5.843,60.000 1.963,60.000 C 1.963,46.272 1.963,32.545 1.963,18.816 Z" />
      </Canvas>
      <TextBlock Grid.Column="1" x:Name="PageTitle" Text="{Binding About}" 
      Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}" />
    </Grid>
  </StackPanel>

  <!--ContentPanel - place additional content here-->
  <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    <Grid.RowDefinitions>
      <RowDefinition />
      <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <StackPanel>
      <StackPanel Orientation="Horizontal">
        <TextBlock Text="{Binding AppTitle}" 
          FontSize="{StaticResource PhoneFontSizeLarge}" 
          FontFamily="{StaticResource PhoneFontFamilySemiBold}" 
          VerticalAlignment="Bottom" 
        Foreground="{StaticResource PhoneAccentBrush}" />
        <TextBlock Text="{Binding ApplicationVersion}" Margin="12,0,0,2" 
        FontSize="{StaticResource PhoneFontSizeMediumLarge}" 
          VerticalAlignment="Bottom" 
        FontFamily="{StaticResource PhoneFontFamilySemiBold}" />
      </StackPanel>
      <TextBlock TextWrapping="Wrap" Text="{Binding Copyright}" />
      <ScrollViewer Margin="0,10,0,0">
        <StackPanel Orientation="Vertical">
          <TextBlock Text="{Binding SupportMessage}" TextWrapping="Wrap" 
            d:LayoutOverrides="Width" />
          <Button x:Name="MailButton" Padding="17,12">
            <i:Interaction.Triggers>
              <i:EventTrigger EventName="Click">
                <GalaSoft_MvvmLight_Command:EventToCommand 
                  Command="{Binding SupportQuestionCommand, Mode=OneWay}"/>
              </i:EventTrigger>
            </i:Interaction.Triggers>
            <Canvas xmlns="http://schemas.microsoft.com/client/2007" 
              Width="23.903" Height="18.441">
              <Path Fill="{StaticResource PhoneForegroundBrush}" Data="F1 M 2.446,15.994 L 2.446,5.334 L 11.021,12.021 C 11.243,12.193 11.510,12.279 11.774,12.279 C 12.038,12.279 12.303,12.193 12.524,12.023 L 21.457,5.100 L 21.457,15.994 L 2.446,15.994 Z M 20.883,2.447 L 11.776,9.506 L 2.728,2.447 L 20.883,2.447 Z M 22.678,0.000 L 1.221,0.000 C 0.547,0.000 0.000,0.547 0.000,1.223 L 0.000,17.217 C 0.000,17.893 0.547,18.441 1.221,18.441 L 22.678,18.441 C 23.354,18.441 23.903,17.893 23.903,17.217 L 23.903,1.223 C 23.903,0.547 23.354,0.000 22.678,0.000" />
            </Canvas>
          </Button>
          <StackPanel x:Name="BuyPanel" Orientation="Vertical" 
            Visibility="{Binding BuyPanelVisible}">

            <TextBlock TextWrapping="Wrap" Text="{Binding BuyTheApp}"/>
            <Button x:Name="BuyButton" Content="{Binding Buy}">
              <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                  <GalaSoft_MvvmLight_Command:EventToCommand 
                    Command="{Binding BuyCommand, Mode=OneWay}"/>
                </i:EventTrigger>
              </i:Interaction.Triggers>
            </Button>
          </StackPanel>
          <TextBlock HorizontalAlignment="Left" TextWrapping="Wrap" 
             Text="{Binding ReviewTheApp}"/>
          <Button x:Name="ReviewButton" Content="{Binding Review}">
            <i:Interaction.Triggers>
              <i:EventTrigger EventName="Click">
                <GalaSoft_MvvmLight_Command:EventToCommand 
                  Command="{Binding ReviewCommand, Mode=OneWay}"/>
              </i:EventTrigger>
            </i:Interaction.Triggers>
          </Button>
        </StackPanel>
      </ScrollViewer>
    </StackPanel>
    <HyperlinkButton Content="{Binding CompanyUrl}" Grid.Row="1" 
       Margin="0,0,0,6" >
      <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click">
          <GalaSoft_MvvmLight_Command:EventToCommand 
            Command="{Binding ViewWebsiteCommand, Mode=OneWay}"/>
        </i:EventTrigger>
      </i:Interaction.Triggers>
    </HyperlinkButton>
  </Grid>
</Grid>

which you might as well forget about and just copy it from the sample solution which I uploaded. The net result is something like this in trial mode:

aboutsample

If your app is not running in trial mode, the text “If you prefer the full version… “ and the “buy” button are invisible. Of course, you can change the texts that are displayed by changing the resource, or you can have a ball and make your own stylish look & feel for an about page. It’s up to you. But using a resource file, my base class and Manfred’s XAML you can make a fully functional about page in next to no time, and there is no excuse now for failing section 5.6 ever again.

Update: you might want to change "trail" into "trial" in your resource file ;-). Thanks to Matthijs Hoekstra for spotting this.

3 comments:

KeepingItSimple said...

Thanks a Million for this very very useful post !!!

Absolutely a great guidelines for all WP7 app developers.

Regards

KRK

Anonymous said...

Sweet.

sumith said...

Great, The code i was looking for and will repeat this forever ” Thou shalt not make unto thee any Windows Phone 7 application without MVVMLight”