Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

WPF: How hard could it be to get a SelectedItemTemplate?


:P
On this page:

Here's a fun one to do in WPF. Say you have a ListBox or ListView that is databound and you want to get at the content of the data template used for binding of the items in the list. In my case I want to trigger an animation when the item is selected to indicate the change.

It's quite easy to get a selected item from a list box: 

// *** Bound item - ie. an XmlNode/XmlElement

XmlElement item = this.lstPhotos.SelectedItem as XmlElement;

if (item == null)

    return;

This assumes the listbox is bound with XML - in this case using an XmlDataProvider going against an XML I pull down from my Web site. That's great if all you're after is the data that you've bound to, but if you also need access to the data template or items in the data template that make up an individual list item, there's no simple way to retrieve that information.

For background, here's what I'm working off of: For experimentation (mostly for animation related stuff which I left out here) I created a simple form with a listbox on it. The list pulls down an XML file for any one of my photoalbum pages which can then be viewed in this list. The idea is I can point at any of my photoalbums and quickly review the images - and eventually use it as an organizer for administration to allow a few things like uploading and resorting of images. For now the task is to display the images in WPF.

Here's what the very simple app looks like:

 

The app basically has a URL property at the top which points at a photoalbum folder on the Web and go button to reload the list. When an item is clicked the image slides out to show a bigger preview, which goes back to its original size as the item is deselected.

The XAML for this form looks like this:

 

<Window x:Class="WPFAnimation.PhotoViewer"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:WPFAnimation="clr-namespace:WPFAnimation" 
    Title="PhotoViewer" Height="700" Width="500" 
    x:Name="frmPhotoViewer"
    Loaded="Window_Loaded"
    >
<Window.Resources>  
<XmlDataProvider IsAsynchronous="True" 
                 IsInitialLoadEnabled="True"                                               
                 Source="http://www.west-wind.com/rick/photoalbum/hoodriver2006/photoalbum.xml" 
                 XPath="/PhotoAlbum/Photos/Photo" x:Key="PhotoSource"/>
 <WPFAnimation:ImageUrlConverter x:Key="ImageUrlConverter"  />      
 <Storyboard x:Key="ImageAnimation">
      <DoubleAnimation To="160" Duration="0:0:.5" AutoReverse="True" DecelerationRatio=".80"/>
</Storyboard>        
</Window.Resources>
    <DockPanel>             
        <StackPanel Orientation="Horizontal" VerticalAlignment="Center" DockPanel.Dock="Top">
            <Label Height="25">Base Url:</Label>
            <TextBox  x:Name="txtUrl" Text="http://www.west-wind.com/rick/photoalbum/hoodriver2006/" Height="25" Margin="0,0,10,0" />
            <Button x:Name="btnUpdateUrl" Click="OnUpdateUrl" Content="Go" />
        </StackPanel>
        
        <ListBox x:Name="lstPhotos" 
                 ItemsSource="{Binding Source={StaticResource PhotoSource},IsAsync=True }" 
                 Background="AliceBlue" 
                 SelectionChanged="OnSelectionChanged" >
          <ListBox.ItemTemplate>
              <DataTemplate>
                  <StackPanel Margin="0,5,0,5"  x:Name="spOuterPanel">                 
                    <StackPanel Orientation="Horizontal"  VerticalAlignment="Top">                        
                    <Image Stretch="Uniform" Width="150" HorizontalAlignment="Left" x:Name="imgImage">
                      <Image.Source>
                          <MultiBinding Converter="{StaticResource ImageUrlConverter}">
                              <Binding Mode="OneWay" XPath="FileName" />                                                                        
                              <Binding ElementName="frmPhotoViewer"  Path="BasePath" />
                          </MultiBinding>
                      </Image.Source>                    
                      <Image.Triggers>
                          <EventTrigger RoutedEvent="StackPanel.MouseLeave">
                              <BeginStoryboard>
                                  <Storyboard TargetName="imgImage" TargetProperty="Width">
                                      <DoubleAnimation To="150" Duration="0:0:0.5" />
                                  </Storyboard>
                              </BeginStoryboard>
                          </EventTrigger>
                      </Image.Triggers>
                    </Image>
                      <StackPanel Margin="10,0,0,0">
                        <TextBlock Text="{Binding XPath=Notes}" TextWrapping="Wrap" Width="200" />
                        <TextBlock><Hyperlink Click="OnFullImageClick">Show full image</Hyperlink></TextBlock>                
                      </StackPanel>
                      </StackPanel>
                      <Image Source="{Binding Converter={StaticResource ImageUrlConverter}, XPath=ImageUrl, Mode=OneWay}"  />
                  </StackPanel>            
              </DataTemplate>
          </ListBox.ItemTemplate> 
        </ListBox>    
                        
    </DockPanel>
</Window>

The XAML is a little more complex than it needs to be due to some special databinding required in order to make the image display work correctly as mentioned in a previous post - basically the XML document doesn't contain the full image path so some extra databinding hookups are required to make the image display work.

The main portion of the page is the ListBox control which is data bound to the XmlDataProvider, which is super easy to do. Even nicer is the fact that the data source AND the databinding happen asynchronously without anything more than specifying the fact both on the data provider and the listbox binding!

The listbox, then uses a DataTemplate to provide the actual photo and description layout for each list item. Each piece is individually bound to via XPath binding to the data source which also is super easy - as long as you can bind to a direct value. As mentioned in the previous post the image is a bit more complex and required creation of a custom MultiValueConverter in order to handle the path and filename combination to properly get an image path to bind to the Image's Source property. Other than that the layout is pretty straight forward - although fairly nested in order to lay out properly.

Now, as I mentioned I need to trap the selection in the list box, so when the list box item is selected the image 'slides out' and grows to specific width. It's possible to do this via triggers in Xaml but a few other things needed to happen as well to update the content. While it's easy to get the Selected data item (which is what lstPhotos.SelectedItem will return) that item is not the data template, but rather the data item - or an XmlElement in this case.

To get a hold of the data template a bit more work is required:

 

protected void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{            
    // *** Get the bound item - ie. an XmlNode/XmlElement  - for our Data
    XmlElement item = this.lstPhotos.SelectedItem as XmlElement;
    if (item == null)
        return;

    // *** Get the ListBoxItem 
    ListBoxItem listItem =  this.lstPhotos.ItemContainerGenerator.ContainerFromIndex(this.lstPhotos.SelectedIndex) as ListBoxItem;
    if (listItem == null)
        return;

    // *** Retrieve the ContentTemplate which is the DataTemplate
    DataTemplate dt = listItem.ContentTemplate;
    
    // *** Retrieve the first child which is a Border (intuitive eh?)
    Border b = VisualTreeHelper.GetChild(listItem, 0) as Border;
    
    // *** Get the child of the border which is the actual content
    ContentPresenter cp = b.Child as ContentPresenter;

    // *** Find the outer container StackPanel in the content
    StackPanel sp = dt.FindName("spOuterPanel",cp) as StackPanel;

    // *** Find the Image in the nested content (note: FindName does recursion!)
    Image img = sp.FindName("imgImage") as Image;

    // *** Assign a new image source from the combination of a property and data item
    img.Source = new ImageSourceConverter().ConvertFromString(
                          this.BasePath + item["FileName"].InnerText) as ImageSource;

    // *** Animate the image
    DoubleAnimation ani = new DoubleAnimation(350,new Duration( TimeSpan.FromSeconds(1))  );
    ani.DecelerationRatio = .5;                  
    img.BeginAnimation(Image.WidthProperty, ani);            
}

Ei, ei, ei. That's a mouthful huh? And no I didn't figure this out on my own - I had help from Microsoft and Yi-Lun Luo on the MSDN forums. Heck this is not the sort of thing you figure out on your own I think <g>...

Basically there's no direct way to get at the content template, so what this code does is methodically digs into the hierarchy to get at the template. What's interesting is that if you try to follow this through just by looking in the debugger this is not discoverable, because most of these things end up working only when items get cast to very specific types. WPF objects in general seem very difficult to debug because they have so many properties that are not accessible or so deeply buried in the inheritance tree that it takes 10 levels of base steps to get to them... Ouch.

Sure would be nice if this sort of thing was exposed on the SelectedItem or at the very least as part of the SelectionChangedEventArgs. Or a SelectedItemTemplate property.

There's also no other selection mechanism supported for ListBox or ListView than SelectionChanged. So while you can detect a SelectionChanged event there's no specific event that lets you detect when you 'reclick' an already selected item. Or a DoubleClick event for that matter. At this point you have to rely on MouseDown. But you can't JUST rely on MouseDown because if you do the selection won't be changed yet so you won't be able to pick up the current selection. And apparently using both MouseDown and SelectedIndexChanged in combination makes the selection event not fire consistently. Great.

BTW, speaking of the event handling I noticed that WPF has the same crappy mouse event handling that HTML has where MouseEnter and MouseLeave for a top level container are fired for that container when entering child controls. So if you have MouseEnter and MouseLeave set on the above StackPanel and you then slide the mouse into the inner image it's considered a MouseLeave event for the StackPanel. With this behavior it's really hard to build consistent animation effects because you can never really tell when the mouse ACTUALLY leaves the container unless you write a bunch of code to check what you are leaving. So much for using triggers for that sort of thing.

It never ceases to amaze me that for all the thoughtfulness that has obviously gone into the bowels of WPF, that when it comes to the more high level UI constructs so little thought is given to the final mile to implementation. It's important that the plumbing works right, but making the UI level easy with common scenarios seems to me is just as important. Everywhere I turn in WPF it seems to me that that last mile is missing. I mentioned it the other day in the databinding stuff that can only bind declaratively to non-aggregate values, and here once again the same adage applies where simple scenarios become increasingly difficult because a couple of essential events are missing. <sigh>

Example Code (note: project is set up for Orcas but code works with VS2005)

Posted in WPF  

The Voices of Reason


 

Polo's Blog
June 28, 2007

# Polo's Blog: WPF: getting a data-bound data template and the items within it


Rick Strahl's Web Log
July 16, 2007

# Rick Strahl's Web Log


DaveI
August 09, 2007

# re: WPF: How hard could it be to get a SelectedItemTemplate?

Great article similar problem. I have the following (should be easy) xaml:
<TabControl Name="myTab"
ItemTemplate="{StaticResource HeaderTemplate}"
ContentTemplate="{StaticResource ContentTemplate}" >
</TabControl>

ContentTemplate is in a merged Resourcedictionary. ContentTemplate contains a listbox. Can I get SelectionChanged..no I cannot. The error actually tells me to remove the event handler, the code would then compile but not really helpful. Is there an easier route or do I have to do as you have above?

joe
July 06, 2008

# re: WPF: How hard could it be to get a SelectedItemTemplate?

"There's also no other selection mechanism supported for ListBox or ListView than SelectionChanged. So while you can detect a SelectionChanged event there's no specific event that lets you detect when you 'reclick' an already selected item. Or a DoubleClick event for that matter."

The ListView does have a double click event. What I've done is just handle that event and just check of the selected index is greater than 0 and if so, get the first item. It has worked better for me than handle the mouse down. Like this:

private void Window_Loaded(object sender, RoutedEventArgs e)
{
lvVideos.MouseDoubleClick += new MouseButtonEventHandler(lvVideos_MouseDoubleClick);
}

void lvVideos_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (lvVideos.SelectedItems.Count > 0)
{
ListViewItem lvi = lvVideos.SelectedItems[0] as ListViewItem;

// do stuff with your item here
}
}

Eric Coulson
December 10, 2008

# re: WPF: How hard could it be to get a SelectedItemTemplate?

This article helped me get on the right track - but the ContentTemplate of ListBox was null.

to solve this problem:

foreach (XmlElement item in MyListBox.Items)
{

ListBoxItem listItem = YAxisTemplateListBox.ItemContainerGenerator.ContainerFromItem(item) as ListBoxItem;
// I added next line to get non null dt (DataTemplate)
DataTemplate dt = ((FrameworkElement)MyListBox).FindResource(item.LocalName) as DataTemplate;
// *** Retrieve the first child which is a Border (intuitive eh?)
Border b = VisualTreeHelper.GetChild(listItem, 0) as Border;
// *** Get the child of the border which is the actual content
ContentPresenter cp = b.Child as ContentPresenter;

Border border = (Border)dt.FindName("_MyMainBorder", cp);
Canvas canvas = (Canvas)dt.FindName("MyCanvas", cp);
Button button = (Button)dt.FindName("_MyButton", cp);
}

Alvaro Suarez
September 17, 2009

# re: WPF: How hard could it be to get a SelectedItemTemplate?

Great article. Made my work much much easier. Thank you.

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024