If you are doing Windows Phone 7 development, chances are that you are either using or have considered using the Panorama. To make a truly glorious Panorama application, to immerse the user in the experience, or for branding purposes, many applications pay a lot of attention to the background. The Panorama has a Background property of type Brush. Although this is most often used with an ImageBrush, it can also be a SolidColorBrush or a BradientBrush. Using gradients is not recommended, unless you are a fan of color banding. It would be nice if the ImageBrush could be used with a WriteableBitmap, but that doesn’t work. But there is something better…

Unlocking Panorama.Background to allow arbitrary content

If you examine Panorama.UpdateBackground using Reflector, you can see that the background ContentPresenter’s Height is always set to the viewport’s height. It handles a Background that is set to a SolidColorBrush by setting width of the background’s ContentPresenter to the viewport width; if it is a GradientBrush, it sets the width to the width of the items; for ImageBrush it does some magic to wait for the image to be loaded, and then sets the background’s width to the pixel width of the image. It also sets a property ominously called “IsStatic”. Keen observers will also be able to see why WriteableBitmaps can’t be used as the ImageSource of your ImageBrush. But you don’t really want that anyway, because the perf is not good, and anything you can do with a WriteableBitmap, by definition you can do with XAML.

If you want to be able to put arbitrary XAML in the background, you need to somehow do two things:

  1. set the width of the background’s ContentPresenter
  2. set IsStatic to false

You can get this done in code, but it is a bit of a pain, and frankly, a little sketchy. Fortunately, there’s an easier way to do this. Remember the good old days of getting stuff to go where you want it in HTML by using 1 x 1 pixel transparent GIFs? Well, I don’t propose doing anything as old-school-hacky as that. No, this method uses a modern 1 x N transparent PNG (where N is the desired width of your background.) OK, I guess this is a little on the sketchy side, too, but like I said, it has the virtue of being easy.

The attached solution includes “dummy.png”, an 800 x 1 transparent PNG that I created using Paint.NET. It is included in the project, and the build action is set to “Resource”. In the main page’s XAML, the Panorama.Background is set to an ImageBrush that has dummy.png as its ImageSource. The other thing that we have to do is to re-template the Panorama, to include our new spiffy XAML background. Here’s what the page looks like:

Code Snippet
  1. <phone:PhoneApplicationPage
  2.     x:Class="PanoramaXamlBackground.MainPage"
  3.     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  4.     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  5.     xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
  6.     xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
  7.     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  8.     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  9.     xmlns:controls="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls"
  10.     xmlns:controlsPrimitives="clr-namespace:Microsoft.Phone.Controls.Primitives;assembly=Microsoft.Phone.Controls"
  11.     mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="800"
  12.     FontFamily="{StaticResource PhoneFontFamilyNormal}"
  13.     FontSize="{StaticResource PhoneFontSizeNormal}"
  14.     Foreground="{StaticResource PhoneForegroundBrush}"
  15.     SupportedOrientations="Portrait" Orientation="Portrait"
  16.     shell:SystemTray.IsVisible="False">
  17.  
  18.     <phone:PhoneApplicationPage.Resources>
  19.         <ControlTemplate x:Key="panoramaTemplate" TargetType="controls:Panorama">
  20.             <Grid>
  21.                 
  22.                 <Grid.RowDefinitions>
  23.                     <RowDefinition Height="auto"/>
  24.                     <RowDefinition Height="*"/>
  25.                 </Grid.RowDefinitions>
  26.  
  27.                 <controlsPrimitives:PanningBackgroundLayer x:Name="BackgroundLayer" Grid.RowSpan="2" HorizontalAlignment="Left">
  28.                     <Border x:Name="background" CacheMode="BitmapCache">
  29.                         <Grid Background="Transparent">
  30.                             <Rectangle Width="400" Height="200" Fill="Blue" Opacity=".5" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="32"/>
  31.                             <Rectangle Width="300" Height="600" Fill="Green" Opacity=".5" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="300,128"/>
  32.                             <Rectangle Width="600" Height="100" Fill="Red" Opacity=".5" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="200,500,0,0"/>
  33.                             <Ellipse x:Name="indicator" Width="50" Height="50" Fill="Orange" Opacity=".5" HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="16">
  34.                                 <Ellipse.Triggers>
  35.                                     <EventTrigger RoutedEvent="Ellipse.Loaded">
  36.                                         <BeginStoryboard>
  37.                                             <Storyboard Storyboard.TargetName="indicator" Storyboard.TargetProperty="Opacity">
  38.                                                 <DoubleAnimation To="0" AutoReverse="True" RepeatBehavior="Forever"/>
  39.                                             </Storyboard>
  40.                                         </BeginStoryboard>
  41.                                     </EventTrigger>
  42.                                 </Ellipse.Triggers>
  43.                             </Ellipse>
  44.                         </Grid>
  45.                     </Border>
  46.                 </controlsPrimitives:PanningBackgroundLayer>
  47.  
  48.                 <controlsPrimitives:PanningTitleLayer x:Name="TitleLayer" Grid.Row="0" HorizontalAlignment="Left" Margin="10,-76,0,9"
  49.                                                         Content="{TemplateBinding Title}" ContentTemplate="{TemplateBinding TitleTemplate}"
  50.                                                         FontSize="187" FontFamily="{StaticResource PhoneFontFamilyLight}" CacheMode="BitmapCache"/>
  51.  
  52.                 <controlsPrimitives:PanningLayer x:Name="ItemsLayer" Grid.Row="1" HorizontalAlignment="Left">
  53.                     <ItemsPresenter x:Name="items"/>
  54.                 </controlsPrimitives:PanningLayer>
  55.  
  56.             </Grid>
  57.         </ControlTemplate>
  58.     </phone:PhoneApplicationPage.Resources>
  59.     
  60.     <Grid x:Name="LayoutRoot">
  61.  
  62.         <controls:Panorama Template="{StaticResource panoramaTemplate}" Title="xaml background">
  63.             <controls:Panorama.Background>
  64.                 <ImageBrush ImageSource="dummy.png"/>
  65.             </controls:Panorama.Background>
  66.  
  67.                     <controls:PanoramaItem Header="home"/>
  68.             <controls:PanoramaItem Header="item 1"/>
  69.             <controls:PanoramaItem Header="item 2"/>
  70.         </controls:Panorama>
  71.     
  72.     </Grid>
  73.     
  74. </phone:PhoneApplicationPage>

You can see the Panorama’s Background being set to dummy.PNG (line 64). The new background is set in lines 29-44. Although this doesn't need it, not having any colors near the edges, you can also apply a margin to prevent seaming. Here there is a reward for the keen observer. You will notice that there is a subtle animation on the Ellipse. Yes, this is a Panorama with an animated background. But please, if you’re going to travel down that path, keep it subtle, and watch that frame rate counter. And beware of wrapping artifacts. The Panorama is not designed to have a dynamic background. And keep both light and dark themes in mind.