18 February 2014

Build for both–an instruction video pivot page for Windows Phone apps

For Windows Phone apps, the same goes a for Windows 8 store apps – a picture tells more than a thousand words and a video even more. So as promised earlier, I am going to show you how to make a video instruction pivot for Windows Phone, that works as showed here below.

Windows Phone video instruction demo

The Pivot part is easy, as this is already built-in – controlling the MediaElements proved to be substantially harder.

A recap of the objectives:

  • Start the video on the current visible pivot item automatically – and start the first video on the first pivot as soon as the user access the page.
  • Repeat that movie automatically when it ends
  • Stop it as soon as the user selects a different pivot item

Setting the stage

  • Create a blank Windows Phone app
  • Bring in my WpWinNl library from Nuget
  • Add an empty page “Help.xaml”
  • Open WMAppManifest.xml, look for the box “Navigation Page” and change this from “MainPage.xaml” to “Help.xaml”. This will make your app start the Help page on startup. That’s not a smart move for production code but it will make a demo app a lot easier.

The XAML basics

Basically the page (expect for the header) is filled by a StackPanel containing a PivotItem and some styling for (mostly) the MediaElementand the PivotItem. That gives every MediaElement a specific alignment and size that will make it fit just in a portrait-oriented page.

<StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,30,0,0">
  <TextBlock x:Name="ApplicationTitle" Text="MY APP" 
             Style="{StaticResource PhoneTextNormalStyle}" Margin="0,0,30,0"/>
  <phone:Pivot x:Name="VideoPivot" Title="How to do stuff" 
               HeaderTemplate="{StaticResource HeaderTemplate}" Margin="-25,0,0,0">
    <phone:Pivot.Resources>
      <Style TargetType="MediaElement">
        <Setter Property="Margin" Value="12,0,0,0"></Setter>
        <Setter Property="VerticalAlignment" Value="Top"></Setter>
        <Setter Property="HorizontalAlignment" Value="Left"></Setter>
        <Setter Property="Width" Value="478"></Setter>
        <Setter Property="Height" Value="268"></Setter>
      </Style>
      <Style TargetType="phone:PivotItem">
        <Setter Property="CacheMode" Value="{x:Null}"></Setter>
      </Style>
    </phone:Pivot.Resources>
    <phone:PivotItem Header="connect via tap+send">
      <MediaElement Source="Video/2PhonePongConnect.mp4" />
    </phone:PivotItem>
  <!-- More PivotItems with video -->
  </phone:Pivot>
</StackPanel>

Also note that the styling turns off CacheMode for the Pivot items. That seems to be necessary. The MediaElements themselves contain, apart from the url to the video, nothing else. The rest is set from code behind.

You might also notice that the Pivot used a HeaderTemplate: that’s fairly simple and just exists to limit the header text size a little:

<DataTemplate x:Key="HeaderTemplate">
   <TextBlock Text="{Binding}" FontSize="35"/>
</DataTemplate>

And again some code to make it work

There is an important difference between the FlipView and the Pivot – in what I think is an effort to conserve memory, not all the pivot panes are loaded into memory at startup: only the selected one (i.e. the first), and the ones left en right of that. So we have no way of retrieving all the MediaElements up front. So we have to gather and attach to those elements as we go along.

There is also an important issue with MediaElement on Windows Phone – it does not like to have multiple initialized MediaElements on one page. So I designed all kind of trickery to make this work. For starters, in order to make this work, we need a dictionary of all the video elements and their sources:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using WpWinNl;
using WpWinNl.Utilities;

namespace TwoPhonePong
{
  public partial class Help
  {
    private readonly Dictionary<MediaElement, Uri> sources = 
new Dictionary<MediaElement, Uri>(); } }
The constructor, of course, kicks off the whole process
public Help()
{
  InitializeComponent();
  VideoPivot.Loaded += VideoPivotLoaded;
  VideoPivot.LoadedPivotItem += VideoPivotLoadedPivotItem;
}
Then comes the funky stuff:
private void VideoPivotLoaded(object sender, RoutedEventArgs e)
{
  VideoPivot.GetVisualDescendents().OfType<MediaElement>().
ForEach(StoreMediaElementReference); PlayMovieOnPivotItem((PivotItem)VideoPivot.SelectedItem); } private void StoreMediaElementReference(MediaElement mediaElement) { sources.Add(mediaElement, mediaElement.Source); mediaElement.Source = null; mediaElement.AutoPlay = false;

mediaElement.MediaEnded += MovieMediaElementMediaEnded; mediaElement.MediaOpened += MovieMediaElementMediaEnded; }

I case you are wondering about your sanity (or mine) – don’t worry, what you seems to be reading is correct: VideoPivotLoaded finds all the active MediaElements on the Pivot, an the calls StoreMediaElementReference – which stores the URL of their source in the sources directory and then clears the MediaElement’s Source (basically un-initializing it) and sets AutoPlay to false. And then it add the MovieMediaElementMediaEnded method to both the MediaEnded and MediaOpened events of the MediaElement. It does this – of course – only for the PivotItems that are actually loaded.

In case you think thinks can’t get any weirder than this, wait till you see the PlayMovieOnPivotItem method

private void PlayMovieOnPivotItem(PivotItem e)
{
  var mediaElement = e.GetVisualDescendents().
OfType<MediaElement>().FirstOrDefault(); VideoPivot.GetVisualDescendents().OfType<MediaElement>(). Where(p => p != mediaElement).ForEach( p => { p.Stop(); p.Source = null; }); if (mediaElement != null) { mediaElement.Position = new TimeSpan(0); if (!sources.ContainsKey(mediaElement)) { StoreMediaElementReference(mediaElement); } mediaElement.Source = sources[mediaElement]; }

So, first it tries to find the MediaElement on the current PivotItem. Then it stops all the MediaElements that are not on this Pivot, and clears their source – effectively as I said, uninitializing them. For the MediaElement on the current PivotItem – if it’s not in the sources dictionary (so it was on the fourth or higher PivotItem) it is added to the sources using StoreMediaElementReference. However – the source of the MediaElement – that at this point is null, whether it was in the sources dictionary or not – is now set from that sources dictionary.

If the movie has finished loading the MediaOpened event is fired, and calls the MovieMediaElementMediaEnded method – remember that is was wired up in StoreMediaElementReference?

private void MovieMediaElementMediaEnded(object sender, RoutedEventArgs e)
{
  var mediaElement = sender as MediaElement;
  if (mediaElement != null)
  {
    mediaElement.Position = new TimeSpan(0);
    mediaElement.Play();
  }
}

This method starts video playback it – so as soon as the video loaded, it starts playing. And since this same method is wired up to the MediaEnded event too – this will enable the repeated playback of this video as well. Until it is not by code.

Of course – when a user changes the selected PivotItem by swiping left or right – a new video needs to be started and the other ones stopped. This is pretty easily done now:

private void VideoPivotLoadedPivotItem(object sender, PivotItemEventArgs e)
{
  PlayMovieOnPivotItem(e.Item);
}

This method was wired up to the Pivot’s LoadedPivotItem way up in the constructor of this page.

Your video instruction page now works. Now to prevent memory leaks and especially prevent blocking other MediaElements elsewhere in the application, this method clears all even wiring and the MediaElement’s Source properties

protected override void OnNavigatedFrom(NavigationEventArgs e)
{
  sources.Keys.ForEach(p =>
                       {
                         p.Source = null;
                         p.MediaEnded -= MovieMediaElementMediaEnded;
                         p.MediaOpened -= MovieMediaElementMediaEnded;
                       });
  VideoPivot.Loaded -= VideoPivotLoaded;
  VideoPivot.LoadedPivotItem -= VideoPivotLoadedPivotItem;
  base.OnNavigatedFrom(e);
}

And thus you have created a video instruction page that works on Windows Phone under all circumstances (at least under all circumstances I have encountered).

Enjoy – and find the demo solution here

No comments: