Udostępnij za pośrednictwem


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.

Comments

  • Anonymous
    September 12, 2010
    The comment has been removed

  • Anonymous
    September 12, 2010
    Thanks, Kevin. I've fixed the archive. You can re-download it. -Alex

  • Anonymous
    September 12, 2010
    How would you go about offsetting the animation for each of the visible items in the list view with the picker dialog?  So one items animation follows anothers...

  • Anonymous
    September 13, 2010
    Very nice, and a good example too. I had to do a few things to get it to compile and deploy with the public Beta:

  1. Manually edit the project files to add the missing mscorlib references (it wouldn't let me do it from the IDE)
  2. Comment out some of the Capabilities settings that don't seem to exist in the public Beta (I couldn't deploy to the emulator otherwise)
  3. Temporarily remove the Foreground setting that references PhoneTextBoxForegroundBrush. Of course I'll be undoing 2) and 3) as soon as I've installed the RTM bits (Friday in our time zone...I'm already like a kid on Christmas Eve)
  • Anonymous
    September 13, 2010
    Thank you Kevin for documenting the changes to make it work on beta. Unfortunatelly I don't have any machine that's running beta bits. But you guys just need to wait for a couple more of days :)

  • Anonymous
    October 18, 2010
    I've used your pickerbox for my test/demo app and likes it, works well. I've now tried making my own dialog, but it does not show. The show() funktion executes with no errors but and the backbutton event registers but nothing displays. I've copied your properties for ChildWindowPopus, RootVisual & Show method and done my own design for my dialog and shipped all animation parts for now, and itemsource parts since i dont use any listbox. Any ideas for whats wroong here. ?

  • Anonymous
    November 17, 2010
    Just so you know, the days of the week start with Sunday.

  • Anonymous
    June 06, 2011
    In wp7.1 environment but still 7.0 built the Dialog list disapear only the title is steady. Any idea why?

  • Anonymous
    August 04, 2011
    The comment has been removed

  • Anonymous
    September 25, 2011
    The comment has been removed

  • Anonymous
    January 25, 2012
    There is an issue where if you create a new dialog then try to remove the closed event handler then add it again it will not show the dialog when you do dialog.show, also if you make a new dialog then do dialog = null then try to make a dialog again then the listbox is null.