Jaa


How do I programmatically interact with template-generated elements? Part II

This post shows you how to find a named element within a DataTemplate.

In Part I, we discussed how to find a named element within a ControlTemplate. That was fairly simple; you’d call Template.FindName on the control that the ControlTemplate has been applied to. But if the template is a DataTemplate, then the scenario is a bit more complex. For instance, if you have a data-bound ListBox that uses a DataTemplate, each generated list item has a tree of generated elements (as described by your DataTemplate). In this post, we’ll walk through that scenario and retrieve a named element within the DataTemplate of a certain list item.

Before showing the code that finds the named element, let’s set up our scenario. We have a Button and a ListBox that’s data-bound (if you want to see how this particular ListBox is bound, you can download the attached zip file).

    <Border Margin="15" BorderBrush="Aqua" BorderThickness="2" Padding="8" CornerRadius="5">

      <StackPanel>

        <ListBox Name="myListBox" ItemTemplate="{StaticResource myDataTemplate}"

                 IsSynchronizedWithCurrentItem="True">

          <ListBox.ItemsSource>

            <Binding Source="{StaticResource InventoryData}" XPath="Books/Book"/>

          </ListBox.ItemsSource>

        </ListBox>

        <Button Margin="10"

                Click="DataTemplateFindElement">Get text of textBlock in DataTemplate</Button>

      </StackPanel>

    </Border>

The ListBox uses a simple DataTemplate. The DataTemplate has an element that’s given the name textBlock:

    <DataTemplate x:Key="myDataTemplate">

      <TextBlock Name="textBlock" FontSize="14">

        <TextBlock.Text>

          <Binding XPath="Title"/>

        </TextBlock.Text>

      </TextBlock>  

    </DataTemplate>

This screenshot shows our simple UI:

 

 

Now let’s write the button event handling code so that when we click the button, we retrieve the TextBlock that’s within the DataTemplate of the current list item. To do that, we need to perform the following steps:

1. Get a hold of the current list item:

   // Note that the ListBox must have

   // IsSynchronizedWithCurrentItem set to True for this to work

   ListBoxItem myListBoxItem =

   (ListBoxItem)(myListBox.ItemContainerGenerator.ContainerFromItem(myListBox.Items.CurrentItem));

2. Find the ContentPresenter of that list item by walking through its visual tree [1]:

   ContentPresenter myContentPresenter = FindVisualChild<ContentPresenter>(myListBoxItem);

3. Now you can call FindName on the DataTemplate of that ContentPresenter:

    DataTemplate myDataTemplate = myContentPresenter.ContentTemplate;

    TextBlock myTextBlock = (TextBlock)myDataTemplate.FindName("textBlock", myContentPresenter);

4. Finally, you can do whatever you want to the element you just retrieved. For demonstration purposes, we create a message box to show the content of that the TextBlock:

    MessageBox.Show("The text of the named TextBlock in the DataTemplate of the selected list item: "

        + myTextBlock.Text);

Now we can select an item, click the button, and see the message box that shows the text content of the TextBlock retrieved from the corresponding DataTemplate!

 

 

You can download this project from the attached zip file. Enjoy!

[1] The FindVisualChild method called in step 2:

  private childItem FindVisualChild<childItem>(DependencyObject obj)

  where childItem : DependencyObject

  {

  for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)

  {

  DependencyObject child = VisualTreeHelper.GetChild(obj, i);

  if (child != null && child is childItem)

  return (childItem)child;

  else

  {

  childItem childOfChild = FindVisualChild<childItem>(child);

  if (childOfChild != null)

  return childOfChild;

  }

  }

  return null;

  }

FindElementinDataTemplate_C#_VB.zip

Comments

  • Anonymous
    April 16, 2007
    PingBack from http://joshsmithonwpf.wordpress.com/2007/04/16/a-gem-of-a-wpf-blog/

  • Anonymous
    April 19, 2007
    One question: What changes should I make in the code if my dataTemplate is used by ContentControl's ContentTemplate rather than a ListBox's ItemTemplate? <ContentControl Name="myContentControl"         Content="{Binding Path=Name}"                 ContentTemplate="{StaticResource myDataTemplate}" /> I've been struggling with this for days. Your help would be greatly appreciated.

  • Anonymous
    April 19, 2007
    If it is a ContentControl, the underlying technique is the same. You first look for the ContentPresenter of the ContentControl, then you find the element on the DataTemplate that's set on that ContentPresenter. In your case your code will be something like this: ContentPresenter myContentPresenter = FindVisualChild<ContentPresenter>(myContentControl); DataTemplate myTemplate = (DataTemplate)this.Resources["myDataTemplate"]; // "textBlock" is the name of the element within the DataTemplate TextBlock retrievedTextBlock = (TextBlock)myTemplate.FindName("textBlock", myContentPresenter); // then do whatever to retrievedTextBlock Hope that helps!

  • Anonymous
    May 07, 2007
    I'm not sure if it provides any performance improvement but I think changing these lines DependencyObject child = VisualTreeHelper.GetChild(obj, i);            if (child != null && child is childItem) To this childItem child= VisualTreeHelper.GetChild(  obj,i) as childItem; if (child!=null) Will avoid the extra cast below. This might save some cycles especially in a deep, bushy visual tree.

  • Anonymous
    June 03, 2007
    What changes do to I need to make if I'm using a ListView/GridView combination and each column uses a different DataTemplate - I've modified the code and it will only retrieve the correct info when the first column is click for any other column that is click I recieve the error "This operation is valid only on elements that have this template applied." <GridViewColumn Header="Company" CellTemplate="{StaticResource CompanyName}"/> <GridViewColumn Header="Insured" CellTemplate="{StaticResource InsuredName}"/> For example click on column "Company" works OK,receive good textBlock data click on "insured" and receive above error message I've been looking at this a weeks now, so any input you can give would be greatly appreciated.

  • Anonymous
    June 04, 2007
    I'am still writing i VB :(  (Firms wish) I wound really like it translated to VB. Is that possible for any of you :). Dont know what refers to the where command in vb . bostilling at hotmail.com for a VB example

  • Anonymous
    June 05, 2007
    Hi pgw1959, you want to start by getting to the ListViewItem for one of the non header rows in the ListView. The tree structure for each ListViewItem is as follows: ListViewItem | Border | Grid | GridViewRowPresenter |                                |             ContentPresenter           ContentPresenter Notice how the GridViewRowPresenter hosts the ContentPresenters for the different columns. The CellTemplate you want is applied to the ContentPresenter at the same index (within the VisualChildren collection) as the column# for the selected Header column. Also, I should point out that instead of looking up the DataTemplate resource, you can do this to get the DataTemplate. DataTemplate myDataTemplate = myContentPresenter.ContentTemplate; That should help as well. This makes more sense and I fixed the blog post accordingly. Let me know if I can help further. Thanks!

  • Anonymous
    June 05, 2007
    Hi bomanjii, I just added the VB version of the sample to the zip file. Enjoy! Tina

  • Anonymous
    June 05, 2007
    Thank you Tina just thought you saved me :D .. But can see I´am using the ItemContainerStyle to set the style of the elements instead of the controlTemplate like your example. How Can I tweak the codebehind to look for ItemContainerStyle contra controlTemplate ? /Bo

  • Anonymous
    June 06, 2007
    Thanks wcsdkteam, I've got this working now thanks very much. One last question hopefully; how do I code for column swaping on the grid?

  • Anonymous
    June 06, 2007
    Hi Bo, I may be misunderstanding your question but this blog post is only applicable to templates. Styles only set properties; they do not generate a tree of elements. If you have the style name you can do something like: Dim myStyle As Style = Me.Resources("myContainerStyle") Alternatively, you can do: ' to get the container Dim myListBoxItem As ListBoxItem = Me.myListBox.ItemContainerGenerator.ContainerFromItem(currentListBoxItem) ' to get the style set on the container Dim myStyle As Style = myListBoxItem.Style but in either case, you can't do FindName to find an element within there because it is not a tree of elements. Again, I may have completely missed your question. What is your scenario? Feel free to email "wcsdkblg" and leave a small sample that demonstrates the problem and we can help you out further. Or perhaps you want to use the WPF forum: http://forums.microsoft.com/MSDN/ShowForum.aspx?ForumID=119&SiteID=1

  • Anonymous
    June 07, 2007
    Hi pgw, If you use the index of the selected header within its row presenter, then things should work correctly. Is that not the case for you?

  • Anonymous
    June 07, 2007
    Hi wcsdkteam, I use the header index (as code below) and findname() returns null // Column index int index = view.Columns.IndexOf(column); // Get the non header row ListViewItem listViewItem = (ListViewItem)(ResultListView.ItemContainerGenerator.ContainerFromIndex(index)); // Find the GridViewRowPresenter GridViewRowPresenter contentPresenter = FindVisualChild<GridViewRowPresenter>(listViewItem); // Get the column details same index as header DependencyObject child = VisualTreeHelper.GetChild(contentPresenter, index); // Get the template used for this column cellTemplate = (child as ContentPresenter).ContentTemplate; // Get 1st TextBlock details TextBlock textBlock = cellTemplate.FindName(name, (FrameworkElement)child) as TextBlock; // Get the binding details bind = BindingOperations.GetBinding( textBlock, TextBlock.TextProperty); // Extract the binding details - XML node sortNode = bind.XPath.ToString(); Thanks, pgw1959

  • Anonymous
    June 12, 2007
    Hi pgw1959, just wanted to let you know that we're investigating the issue.

  • Anonymous
    June 13, 2007
    Hi wcsdkteam, If it helps I posted the code in the WPF forum http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=1691968&SiteID=1 Thanks, pgw1959

  • Anonymous
    June 13, 2007
    Hi pgw1959, Turns out the GridViewRowPresenter does not change the sequence of its children when the columns are swapped. To work around this you need to do this (this solution relies on a certain internal implementation (ordering of children in the GridViewRowPresenter) which can be changed in the future): // GridViewRowPresenter has several ContentPresenters as its visual children // Get the ContentPresenter that is at the column of the clicked Header (using the workaround) // gvrPresenter is the GridViewRowPresenter of the ListViewItem // clickedHeader is the GridViewColumnHeader that's been selected ContentPresenter contentPresenter = VisualTreeHelper.GetChild(gvrPresenter, _actualIndexMap[clickedHeader.Column]) as ContentPresenter; // Get the DataTemplate on that ContentPresenter DataTemplate cellTemplate = contentPresenter.ContentTemplate; The workaround: GridView gridView = ((GridView)myListView.View); for (int i = 0; i < gridView.Columns.Count; i++) {    _actualIndexMap[gridView.Columns[i]] = i; } with _actualIndexMap being: Dictionary<GridViewColumn, int> _actualIndexMap = new Dictionary<GridViewColumn, int>(); This came from my example so you may have to tweak it a bit to apply it on your code.

  • Anonymous
    June 14, 2007
    The comment has been removed

  • Anonymous
    June 14, 2007
    Hi wcsdkteam, From further tests it appears that the 5th column that uses a CellTemplate, no matter where it positoned within all columns always return NULL for the call to... ListViewItem listViewItem = (ListViewItem)(ResultListView.ItemContainerGenerator.ContainerFromIndex(index)); Thanks, pgw1959

  • Anonymous
    June 15, 2007
    Hi pgw1959, Unfortunately, I'm not able to repro this problem. What is index in the above example? Note that index should be ResultsListView.SelectedIndex. Also, do you mean you're encountering this problem after the workaround? Or the problem occurs with or without the workaround? If you're interested, please send a small sample with the steps that demonstrate the exact problem you're encountering to us at wcsdkblg@microsoft.com. Alternatively, you may want to try the WPF forum as well, so more people may share their insights. Thanks!

  • Anonymous
    June 17, 2007
    The comment has been removed

  • Anonymous
    February 05, 2008
    Hi, I found above solution, this helped a lot. In the context of TreeViewItem.HeaderTemplate (used to display tree nodes with image+text) it works almost as desired. Still: When I try to find the templated image of a node (TreeViewItem) and set the Image programatically, ALL(!) nodes change their image, not just the one I work on. Can somebody explain why? Thanks NRN

  • Anonymous
    May 06, 2008
    有一个Listbox,里面的Item是通过数据模板生成的,如下所示:

  • Anonymous
    September 02, 2008
    Hi ,,, Using the method which is described in the blog . i m triying to bind a custom dropdown(which is inside a expander ) in my code behind . the Control hierarchy is-- ListBox--->DataTemplate -->StackPanel--->Expander---> StackPanel---> StackPanel -->Custom DDL(i have to bind this in code behind) but the problem is that , i m getting the listboxitem as null... Here is my XAML ... <Window.Resources>        <DataTemplate x:Key="DataTemplateAlertList">            <StackPanel x:Name="stplAlertList"  Orientation="Vertical">                <Expander  x:Name="expAlert" Width="650" BorderBrush="Silver"  BorderThickness="1" Opacity="1">                    <StackPanel  x:Name="stplAlert"  Orientation="Horizontal">                        <StackPanel  x:Name="stplSms" Margin="140,0,0,0" Orientation="Vertical">                            <ComboBox Margin="0,10,0,0" Height="20" Width="80" x:Name="ddlSmsTime">                                <ComboBox.ItemsPanel>                                    <ItemsPanelTemplate>                                        <VirtualizingStackPanel />                                    </ItemsPanelTemplate>                                </ComboBox.ItemsPanel>                                <ComboBox.ItemTemplate>                                    <DataTemplate>                                        <TextBlock Text="{Binding}" FontSize="10" Height="20" />                                        </DataTemplate>                                </ComboBox.ItemTemplate>                            </ComboBox>                            <ComboBox Margin="0,10,0,0" Height="20" Width="80" x:Name="ddlEmailTime">                                <ComboBox.ItemsPanel>                                    <ItemsPanelTemplate>                                        <VirtualizingStackPanel />                                    </ItemsPanelTemplate>                                </ComboBox.ItemsPanel>                                <ComboBox.ItemTemplate>                                    <DataTemplate>                                        <TextBlock Text="{Binding}" FontSize="10" Height="20" />                                        </DataTemplate>                                </ComboBox.ItemTemplate>                            </ComboBox>                            <ComboBox Margin="0,10,0,0" Height="20" Width="80" x:Name="ddlIVRTime">                                <ComboBox.ItemsPanel>                                    <ItemsPanelTemplate>                                        <VirtualizingStackPanel />                                    </ItemsPanelTemplate>                                </ComboBox.ItemsPanel>                                <ComboBox.ItemTemplate>                                    <DataTemplate>                                        <TextBlock Text="{Binding}" FontSize="10" Height="20" />                                        </DataTemplate>                                </ComboBox.ItemTemplate>                            </ComboBox>                        </StackPanel>                       </StackPanel>                    <Expander.Header >                        <StackPanel Orientation="Horizontal">                            <TextBlock  Text="{Binding Path=Alert_Name }"  Width="70" />                                    <CheckBox Margin="80,0,0,0" IsEnabled="{Binding Path=Allow_EMail }"  x:Name="chkSmsHeader">SMS</CheckBox>                                    <CheckBox Margin="115,0,0,0" IsEnabled="{Binding Path=Allow_SMS }"  x:Name="chkEmailHeader">Email</CheckBox>                                    <CheckBox Margin="120,0,0,0" IsEnabled="{Binding Path=Allow_IVR }"  x:Name="chkIvrHeader">Ivr</CheckBox>                                </StackPanel>                    </Expander.Header>                </Expander>            </StackPanel>        </DataTemplate>    </Window.Resources>  <Grid ShowGridLines="False"   Height="540" Width="800">        <Grid.RowDefinitions>            <RowDefinition Height="20"/>            <RowDefinition/>            <RowDefinition Height="20"/>        </Grid.RowDefinitions>        <Grid.ColumnDefinitions>            <ColumnDefinition Width="20" />            <ColumnDefinition/>            <ColumnDefinition Width="20"/>        </Grid.ColumnDefinitions>        <ListBox   ItemTemplate="{StaticResource  DataTemplateAlertList}" Grid.Row="1" Grid.Column="1" IsSynchronizedWithCurrentItem="True" ScrollViewer.VerticalScrollBarVisibility="Visible" x:Name="LiAlertList" >        </ListBox>    </Grid> My Code BEhind void  Window1_Loaded(object sender, RoutedEventArgs e)    { /------here i get the ListBoxItem  as null /    ListBoxItem myListBoxItem = (ListBoxItem)(LiAlertList.ItemContainerGenerator.ContainerFromItem(LiAlertList.Items.CurrentItem));    ContentPresenter myContentPresenter = FindVisualChild<ContentPresenter>(myListBoxItem);    DataTemplate myDataTemplate = myContentPresenter.ContentTemplate;    ComboBox myComboBox = (ComboBox)myDataTemplate.FindName("ddlSmsTime", myContentPresenter);         /------Binding the DropDownlist with a data source/    myComboBox.ItemsSource = frequencylist;   }  private childItem FindVisualChild<childItem>(DependencyObject obj)where childItem : DependencyObject   {     if (VisualTreeHelper.GetChildrenCount(obj) > 0)       {        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)        {          DependencyObject child = VisualTreeHelper.GetChild(obj, i);          if (child != null && child is childItem)         return (childItem)child;         else         {          childItem childOfChild = FindVisualChild<childItem>(child);          if (childOfChild != null)           return childOfChild;         }        }      }    return null;   } I m not able to bind  the custom dropdownlist. Can any body plase help me in this , I m bit new to WPF . Regards Gaurav Chaturvedi

  • Anonymous
    July 28, 2010
    What if the elements are not named how do find them?