다음을 통해 공유


Editing controls within an ItemContainer for a ListBox (Silverlight/Windows Phone)

Normally, we place controls on a page, give them names, and edit their contents in the respective code file.  However, what happens if those controls live in an ItemTemplate for the ListBox?  We can easily bind a DataSource to the ListBox and access the data from there.  But, that doesn't solve 100% of the cases.  What happens if we want to bind to something that's not our DataSource - say the state of IsSelected on the ListBox?  In WPF, we can bind to IsSelected using a RelativeSource=FindAncestor.  Unfortunately, that doesn't exist in SilverLight and the Windows Phone SDK.  We must be a bit more creative in our approach.

Enter our scenario.  Say we have a ListBox filled with movie titles.  We bind our ListBox's DataContext to an ObservableCollection<MovieItem>.  We bind the title of the movie to the TextBlock in our ItemTemplate.  When our app boots, we load some fake movie data into our collection and our ListBox is automatically updated.

CODE:

 public partial class MainPage : PhoneApplicationPage
{
   public MainPage()
   {
      InitializeComponent();
 
      // Create movie items
      for( int i = 1; i <= 10; i++ )
         m_movieList.Add( new MovieItem()
         {
            Title = "Saw " + i,
            Description = "This is a long description so that we can see it in the drop down menu. Sorry, I don't have anything better to say.",
            Duration = TimeSpan.FromHours( 2 )
         } );
 
      MovieListBox.DataContext = m_movieList;
   }
 
   MovieItemList m_movieList = new MovieItemList();
}
 
public class MovieItemList : ObservableCollection<MovieItem> { }
 
public class MovieItem
{
   public string Title { get; set; }
   public string Description { get; set; }
   public TimeSpan Duration { get; set; }
}

XAML:

 <phone:PhoneApplicationPage.Resources>
   <local:MovieItemList x:Key="MovieItemListDataSource" d:IsDataSource="True"/>
</phone:PhoneApplicationPage.Resources>
 ...
 <ListBox x:Name="MovieListBox" DataContext="{Binding Source={StaticResource MovieItemListDataSource}}" ItemsSource="{Binding}">
   <ListBox.Resources>
      <DataTemplate x:Key="DataTemplateMovie">
         <StackPanel>
            <TextBlock TextWrapping="Wrap" Text="{Binding Title}" VerticalAlignment="Top" HorizontalAlignment="Left"/>
         </StackPanel>
      </DataTemplate>
   </ListBox.Resources>
   <ListBox.ItemTemplate>
      <StaticResource ResourceKey="DataTemplateMovie"/>
   </ListBox.ItemTemplate>
</ListBox>

This works great.  However, we want to add some advanced functionality.  When an item in the ListBox is selected, we want to drop down a Panel that displays additional information like run-time and a description.  This means we need to know which ListBoxItem is selected.  We don't have the ability to use FindAncestor since we're in SilverLight.  This is where we get a bit creative.  First, we need to add what we'll call a drop down panel to our ItemTemplate.  We place an invisible StackPanel with two TextBlocks for the run-time and description.  When it is invisible, it does not take up any space in our ItemTemplate.  When we turn it visible, the ItemTemplate will grow and act just like a drop down menu.

XAML (add as a child of the title TextBlock):

 <StackPanel x:Name="DropDownPanel" Margin="20,0,0,0" Visibility="Collapsed">
   <TextBlock Margin="0" TextWrapping="Wrap" Text="{Binding Description}"/>
   <TextBlock TextWrapping="Wrap" Text="{Binding Duration}" VerticalAlignment="Top" HorizontalAlignment="Left"/>
</StackPanel>

Now, we need to bind our ListBox's IsSelected to the visibility of the drop down panel.  Let's add a SelectionChanged event handler for our ListBox.  This gives us all the elements that were removed from and added to our selection.  First, we need to get the generated ItemContainer from each MovieItem.  Second, we need to walk our visual tree to find a drop down panel child.  Finally, we can simply toggle the visibility based on the item's selected status.

CODE (add to MainPage class):

 void MovieListBox_SelectionChanged( object sender, SelectionChangedEventArgs e )
{
   MovieItem movieItem = e.AddedItems[ 0 ] as MovieItem;
   ListBoxItem item = MovieListBox.ItemContainerGenerator.ContainerFromItem( movieItem ) as ListBoxItem;
   StackPanel dropDownPanel = FindDropDownPanel( item );
   if( dropDownPanel != null )
      dropDownPanel.Visibility = System.Windows.Visibility.Visible;
}
 
StackPanel FindDropDownPanel( DependencyObject obj )
{
   const string dropDownName = "DropDownPanel";
   for( int i = 0; i < VisualTreeHelper.GetChildrenCount( obj ); i++ )
   {
      DependencyObject childObj = VisualTreeHelper.GetChild( obj, i );
      if( childObj != null && childObj is StackPanel && childObj.GetValue( NameProperty ).Equals( dropDownName ) )
         return childObj as StackPanel;
      else
      {
         StackPanel stackPanel = FindDropDownPanel( childObj );
         if( stackPanel != null )
            return stackPanel;
      }
   }
   return null;
}

Notice we made it specific to a StackPanel and we only did the first added item. This can easily be changed but was done for simplicity's sake. I'm sure this can be done in other ways but hopefully someone finds this simple method useful.