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 (24)

  1. rnair says:

    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.

  2. wcsdkteam says:

    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!

  3. Ivolved says:

    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.

  4. pgw1959 says:

    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.

  5. bostilling@hotmail.com says:

    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

  6. wcsdkteam says:

    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!

  7. wcsdkteam says:

    Hi bomanjii,

    I just added the VB version of the sample to the zip file. Enjoy!

    Tina

  8. bostilling@hotmail.com says:

    Thank you Tina just thought you saved me 😀 .. 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

  9. pgw1959 says:

    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?

  10. wcsdkteam says:

    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

  11. wcsdkteam says:

    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?

  12. pgw1959 says:

    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

  13. wcsdkteam says:

    Hi pgw1959, just wanted to let you know that we’re investigating the issue.

  14. pgw1959 says:

    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

  15. wcsdkteam says:

    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.

  16. pgw1959 says:

    Hi wcsdkteam,

    Thanks as always for getting back to me.

    I’ve implemented your "work around" and it seems to work well, however I’m now suffering from another problem I have several columns define in the gridview (some using templates other not) as below ….

     <ListView x:Name="ResultListView" IsSynchronizedWithCurrentItem="true"

           ItemsSource="{Binding Source={StaticResource DatasetDataView}, XPath=Row}"

           GridViewColumnHeader.Click="GridViewColumnHeaderClickedHandler">

       <ListView.View>

         <GridView x:Name="RiskDisplayView" >

           <GridViewColumn Header="Treaty" DisplayMemberBinding="{Binding XPath=PolicyReference}"/>

           <!– Company –>

           <GridViewColumn Header="Company" CellTemplate="{StaticResource CompanyName}"/>

           <!– Insured –>

           <GridViewColumn Header="Insured" CellTemplate="{StaticResource InsuredName}"/>

           <GridViewColumn Header="Their Ref" DisplayMemberBinding="{Binding XPath=SICSReference}"/>

           <!– Type –>

           <GridViewColumn Header="Type" CellTemplate="{StaticResource TreatyType}"/>

           <!– Broker Details –>

           <GridViewColumn Header="Bkr" CellTemplate="{StaticResource BrokerTemplate}"/>

           <!– Share –>

           <GridViewColumn Header="S.Share %" DisplayMemberBinding="{Binding XPath=SignedShare, Converter={StaticResource fourpointConverter}}"/>

           <!– Inception and Expiry Dates –>

           <GridViewColumn Header="Valid From" DisplayMemberBinding="{Binding XPath=InceptionDate, Converter={StaticResource dateConverter}}"/>

           <GridViewColumn Header="Expiry" DisplayMemberBinding="{Binding XPath=ExpiryDate, Converter={StaticResource dateConverter}}"/>

           <GridViewColumn Header="Cancel Year" DisplayMemberBinding="{Binding XPath=CancelYear}"/>

           <GridViewColumn Header="St" DisplayMemberBinding="{Binding XPath=Status}"/>

           <!– Currency Code, Premium/Limit/Excess/Out Limit –>

           <GridViewColumn Header="CCY" DisplayMemberBinding="{Binding XPath=LimitCurrency}"/>

           <GridViewColumn Header="Premium" DisplayMemberBinding="{Binding XPath=Premium, Converter={StaticResource twopointConverter}}"/>

           <GridViewColumn Header="Limit" DisplayMemberBinding="{Binding XPath=LimitAmount, Converter={StaticResource zeropointConverter}}"/>

           <GridViewColumn Header="Excess/Ded" DisplayMemberBinding="{Binding XPath=ExcessAmount, Converter={StaticResource zeropointConverter}}"/>

           <GridViewColumn Header="Our Limit" DisplayMemberBinding="{Binding XPath=LimitAmount, Converter={StaticResource zeropointConverter}}"/>

         </GridView>

       </ListView.View>

     </ListView>

    When I click column "Bkr" the line…

    ListViewItem listViewItem = (ListViewItem)(ResultListView.ItemContainerGenerator.ContainerFromIndex(index));

    returns null, however if I swap columns "Bkr" and "Type" around, "Bkr" works fine but the line above returns null for "Type" and vice-versa.

    Is there a limit on the number of Cell Templates you can use or I’m coding it wrong?, the basic example (without latest workarounds) I’m using is posted at http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=1691968&SiteID=1

    Thanks again for you help, pgw1959.

  17. pgw1959 says:

    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

  18. wcsdkteam says:

    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!

  19. pgw1959 says:

    Hi wcsdkteam,

    I have change the line…

    ListViewItem listViewItem = (ListViewItem)(ResultListView.ItemContainerGenerator.ContainerFromIndex(index));

    to this…

    ListViewItem listViewItem = (ListViewItem)(ResultListView.ItemContainerGenerator.ContainerFromIndex(ResultListView.SelectedIndex));

    The index was coming from

    index = gridView.Columns.IndexOf(column);

    My little example is now working, including column swapping so thanks very much for your help on this.

    Thanks, pgw1959

  20. NRN says:

    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

  21. pdfw says:

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

  22. Gaurav Chaturvedi says:

    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

  23. Thamza says:

    What if the elements are not named how do find them?