Implementing Picker Box functionality on WP7.

Last time I talked about the missing Combobox control in the Windows Phone 7 platform and the ways it could be replaced if you require similar functionality. Among the replacements I mentioned the Picker Box functionality. Today I am going to show you how this could be implemented.

First of all in the "collapsed" state the Picker Box is nothing more than a button which could be very easily implemented by the following XAML:

 <Button Name="buttonDayOfWeek" Background="{StaticResource PhoneTextBoxBrush}" Style="{StaticResource PickerBoxButton}"

        Click="buttonDayOfWeek_Click" BorderThickness="0" Height="72" 
        HorizontalAlignment="Left" Margin="12,126,0,0"  VerticalAlignment="Top" Width="438">
      <StackPanel Orientation="Horizontal" Width="362">                  
            <TextBlock Margin="-17, 0, 0, 0" Foreground="{StaticResource PhoneTextBoxForegroundBrush}" 
                            Text="{Binding}"  />
      </StackPanel>
</Button>

As you can see, I've removed the button's border and changed the background to color to correspond to the color of the TextBox. However, the default button's behavior is to change the backround color when the button is pressed. If you examine the same behavior in the built-in samples of the Picker Box you will notice that the button doesn't change the background when pressed. It means that we need to change the Button's template by removing the xaml that declares animation on the "Pressed" visual state

         <Style x:Key="PickerBoxButton" TargetType="ButtonBase">
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="BorderBrush" Value="{StaticResource PhoneForegroundBrush}"/>
            <Setter Property="Foreground" Value="{StaticResource PhoneForegroundBrush}"/>
            <Setter Property="BorderThickness" Value="{StaticResource PhoneBorderThickness}"/>
            <Setter Property="FontFamily" Value="{StaticResource PhoneFontFamilySemiBold}"/>
            <Setter Property="FontSize" Value="{StaticResource PhoneFontSizeMediumLarge}"/>
            <Setter Property="Padding" Value="10,3,10,5"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="ButtonBase">
                        <Grid Background="Transparent">
                            <VisualStateManager.VisualStateGroups>
                                <VisualStateGroup x:Name="CommonStates">
                                    <VisualState x:Name="Normal"/>
                                    <VisualState x:Name="MouseOver"/>
                                    <VisualState x:Name="Pressed">                                        
                                        <!--<Storyboard>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentContainer" Storyboard.TargetProperty="Foreground">
                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneBackgroundBrush}" />
                                            </ObjectAnimationUsingKeyFrames>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ButtonBackground" Storyboard.TargetProperty="Background">
                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneForegroundBrush}" />
                                            </ObjectAnimationUsingKeyFrames>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ButtonBackground" Storyboard.TargetProperty="BorderBrush">
                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneForegroundBrush}" />
                                            </ObjectAnimationUsingKeyFrames>
                                        </Storyboard>-->
                                    </VisualState>
                                    <VisualState x:Name="Disabled">
                                        <Storyboard>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentContainer" Storyboard.TargetProperty="Foreground">
                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneDisabledBrush}" />
                                            </ObjectAnimationUsingKeyFrames>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ButtonBackground" Storyboard.TargetProperty="BorderBrush">
                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneDisabledBrush}" />
                                            </ObjectAnimationUsingKeyFrames>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ButtonBackground" Storyboard.TargetProperty="Background">
                                                <DiscreteObjectKeyFrame KeyTime="0" Value="Transparent" />
                                            </ObjectAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualState>
                                </VisualStateGroup>
                            </VisualStateManager.VisualStateGroups>
                            <Border x:Name="ButtonBackground" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="0" Background="{TemplateBinding Background}" Margin="{StaticResource PhoneTouchTargetOverhang}" >
                                <ContentControl x:Name="ContentContainer" Foreground="{TemplateBinding Foreground}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" Padding="{TemplateBinding Padding}" Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}"/>
                            </Border>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style> 

All right. This was relativelly easy. Now to the harder part - we need to create a dialog that would display a list of items to select from. If you know me I am a kind of a person who likes to create reusable components and controls. So I decided to encapsulate this dialog functionality in the custom control - PickerBoxDialog. Let's start from the XAML:

 <ResourceDictionary
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:Phone.Controls;assembly=Phone.Controls"
    xmlns:d="https://schemas.microsoft.com/expression/blend/2008"
 xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"    
    mc:Ignorable="d"
    >
 
    <Style TargetType="local:PickerBoxDialog">
        <Setter Property="Background" Value="{StaticResource PhoneChromeBrush}"/>     
        <Setter Property="Width" Value="480" />
        <Setter Property="Height" Value="800" />
        <Setter Property="Margin" Value="0" />
 
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:PickerBoxDialog">
                    <Grid x:Name="LayoutRoot" Background="{StaticResource PhoneChromeBrush}" Margin="0, 34, 0, 0">
                        <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,30,0,40">
                            <TextBlock x:Name="DialogTitle" Text="MY DIALOG TITLE" Style="{StaticResource PhoneTextNormalStyle}"/>
                        </StackPanel>
 
                        <!--ContentPanel - place additional content here-->
                        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0" >
                            <ListBox Name="listBox" >
                                <ListBox.ItemTemplate>
                                    <DataTemplate>
                                        <StackPanel x:Name="item" Orientation="Horizontal" Margin="5, 24, 0, 24">                                            
                                            <TextBlock Margin="15, 0, 0, 0" Text="{Binding}" FontSize="40" TextWrapping="Wrap" />
                                        </StackPanel>
                                    </DataTemplate>
                                </ListBox.ItemTemplate>
                            </ListBox>
                        </Grid>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
 
</ResourceDictionary>

Nothing exceptional here. In the ControlTemplate section I declare the Grid with two rows. First row contains the TitlePanel that will display the title in the second row we have the ListBox with the TextBlock as item. Let's move to the code for the PickerBoxDialog. The control PickerBoxDialog is derived from the ContentControl. In order to make my life easier when dealing with navigation history and back button behavior I've decided to host this control in the Popup and display it when the Show method is called:

         public void Show()
        {
            if (this.ChildWindowPopup == null)
            {
                this.ChildWindowPopup = new Popup();
 
                try
                {
                    this.ChildWindowPopup.Child = this;
                }
                catch (ArgumentException)
                {
                    throw new InvalidOperationException("The control is already shown.");
                }
            }         
 
            if (this.ChildWindowPopup != null && Application.Current.RootVisual != null)
            {
                // Show popup
                this.ChildWindowPopup.IsOpen = true;
            }
 
            if (RootVisual != null)
            {
                // Hook up into the back key press event of the current page
                ((PhoneApplicationPage)RootVisual.Content).BackKeyPress += new EventHandler<System.ComponentModel.CancelEventArgs>(PickerBoxDialog_BackKeyPress);
            }
        }

In the code above we create a new instance of the Popup, assign the control's instance to its Child property and then show popup. There's one more important stpe that needs to be done - hook up into the Back key press enent of the current page. We need to do that in order to dismiss the dialog if a user presses the back button. Another notable thing in the control is how we can get instances of the TextBlock for the title and the listbox. We can do this in the OnApplyTemplate override:

  public override void OnApplyTemplate()
 {
      base.OnApplyTemplate();
 
      // We are ready to retreive controls from the template
      this.listBox = this.GetTemplateChild("listBox") as ListBox;
      this.titleTextBlock = this.GetTemplateChild("DialogTitle") as TextBlock; 
      // Assign the values
      this.ItemSource = itemSource;
      this.Title = title;
      this.SelectedIndex = selectedIndex;           
      // Hook up into listbox's events
      this.listBox.SelectionChanged += new SelectionChangedEventHandler(listBox_SelectionChanged);        
      this.listBox.Loaded += new RoutedEventHandler(listBox_Loaded);
  }

The most compicated code in the PickerBoxDialog control is the part that deals with the animation of the items in the listbox when the dialog is displayed or dissmissed. You should be able to examine it the sample attached to this post.

Now let's move to the way you can use this control in the application. This is how you can initialize it:

  private void InitPickerBoxDialog()
 {
      dialog = new PickerBoxDialog();
      // Assign data source and title
      dialog.ItemSource = data;
      dialog.Title = "FIRST DAY OF THE WEEK";
      // Hook up into closed event
      dialog.Closed += new EventHandler(dialog_Closed);
 }
 void dialog_Closed(object sender, EventArgs e)
{
    // Dialog closed. Assign the value to the button
    this.buttonDayOfWeek.DataContext = data[dialog.SelectedIndex];
}

On the button click event we just make a call to the Show method:

  private void buttonDayOfWeek_Click(object sender, RoutedEventArgs e)
 {          
      // Display dialog
      dialog.Show();         
 }

And that should be it. Here arr the screenshots:

 The full source code and the sample are available as usual.