locked
Unable to Get Element Inside DataTemplate RRS feed

  • Question

  • I read through the "WPF XAML Namespaces" article at http://msdn.microsoft.com/en-us/library/ms746659.aspx so I thought I understood the namescoping issues involved in obtaining elements inside templates, but I am still unable to get it to work.

    For instance, I have the following deliberately-simplistic XAML which creates a TabControl and a Button under it. The TabControl’s resources include a DataTemplate, intended for use with TabItems. When the Button is clicked, it should add a new TabItem, templated on the DataTemplate, to the TabControl:


    <Window x:Class="Test.wndMain"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:src="clr-namespace:Test"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Height="300" Width="300">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="4*"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
            <TabControl Grid.Row="0" Name="ctrlTab_Room">
                <TabControl.Resources>
                    <DataTemplate x:Key="templTab_Cabinet">
                        <TextBlock Name="txtCup"/>
                    </DataTemplate>
                </TabControl.Resources>
            </TabControl>
            <Button Grid.Row="1" Name="btnAddNewCabinet" Click="btnAddNewCabinet_Click" Content="Add New Cabinet"/>
        </Grid>
    </Window>


    Now, I have a Cabinet.cs class file which defines the bare-bones Cabinet class:


    using System;
    
    namespace Test
    {
        class Cabinet
        {
        }
    }



    And in the code-behind for the window, I have (the code is roughly based on and modified from Microsoft’s own example code given at http://msdn.microsoft.com/en-us/library/system.windows.frameworktemplate.findname.aspx):


    using System;
    using System.Windows;            // Necessary for windows
    using System.Windows.Controls;    // Necessary for controls
    using System.Windows.Media;        // Necessary for VisualTreeHelper
    
    namespace Test
    {
        public partial class wndMain : Window
        {   
            public wndMain()
            {
                InitializeComponent();
            }
    
            private void btnAddNewCabinet_Click(object objSender, RoutedEventArgs args)
            {
                // Create cabinet tab
                TabItem tabCabinet_New = new TabItem();
                tabCabinet_New.ContentTemplate = (DataTemplate)ctrlTab_Room.FindResource("templTab_Cabinet");
                tabCabinet_New.Content = new Cabinet();
               
                // Get cup in cabinet
                tabCabinet_New.ApplyTemplate();
                ContentPresenter objContentPresenter = FindVisualChild<ContentPresenter>(tabCabinet_New);
                DataTemplate templTab_Cabinet = objContentPresenter.ContentTemplate;
                TextBlock txtCup = (TextBlock)(templTab_Cabinet.
                    FindName("txtCup", objContentPresenter));
                txtCup.Text = "foobar";
               
                // Add cabinet to room
                ctrlTab_Room.Items.Add(tabCabinet_New);
            }
    
            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;
            }
        }
    }



    When I run this and try to click the button, I get a NullReferenceException at the FindName() line in the button handler because templTab_Cabinet is null. I tried changing the


    DataTemplate templTab_Cabinet = objContentPresenter.ContentTemplate;


    line to:


    DataTemplate templTab_Cabinet = tabCabinet_New.ContentTemplate;


    or to:


    DataTemplate templTab_Cabinet = (DataTemplate)(ctrlTab_Room.FindResource("templTab_Cabinet"));


    instead, but either way instead gets me an InvalidOperationException ("This operation is valid only on elements that have this template applied.") on the FindName() line.

    How can I get this to work? What am I doing wrong? Thanks in advance.
    Tuesday, November 3, 2009 11:44 PM

All replies

  • To get named element inside of DataTemplate, you need to perform following steps:
    1, Get the DP that the DataTemplate is assigned to; for example ItemsControl.ItemTemplate
    2, Get the template parent which is the item container; for example itemsControl.ItemContainerGenerator.ContainerFromIndex(0)
    3, Use template find name approach such as ItemsControl.ItemTemplate.FindName() method with element name and item container parameters to find the named element inside of DataTemplate

    Please see example below:

    Markup:
    <Window x:Class="DataTemplateNameTest.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="MainWindow" Height="350" Width="525">
        <Window.Resources>
            <DataTemplate x:Key="myDataTemplate">
                <Grid x:Name="myPanel">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition/>
                        <ColumnDefinition/>
                    </Grid.ColumnDefinitions>
                    <TextBlock Text="{Binding Path=Name}"/>
                    <TextBlock Grid.Column="1" Text="{Binding Path=Age}"/>
                </Grid>
            </DataTemplate>
        </Window.Resources>
        <StackPanel>
            <ItemsControl Name="itemsControl" ItemsSource="{Binding}" ItemTemplate="{StaticResource myDataTemplate}"/>
            <Button Content="Find element in DataTemplate" Click="Button_Click"/>
        </StackPanel>
    </Window>

    Code:
    using System.Collections.Generic;
    using System.Windows;
    using System.Diagnostics;
    using System.Windows.Controls;

    namespace DataTemplateNameTest
    {
        /// <summary>
        /// Interaction logic for MainWindow.xaml
        /// </summary>
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
                DataContext = new People();
            }

            private void Button_Click(object sender, RoutedEventArgs e)
            {
                Panel panel = itemsControl.ItemTemplate.FindName("myPanel", itemsControl.ItemContainerGenerator.ContainerFromIndex(0) as FrameworkElement) as Panel;
                Trace.WriteLine(panel.GetType().Name);
            }
        }

        public class People : List<Person>
        {
            public People()
            {
                Add(new Person() { Name = "Tom", Age = 10 });
                Add(new Person() { Name = "Ken", Age = 20 });
                Add(new Person() { Name = "Jen", Age = 30 });
            }
        }
        public class Person
        {
            public string Name { set; get; }
            public int Age { set; get; }
        }
    }


    William
    Wednesday, November 4, 2009 12:55 AM
  • To get named element inside of DataTemplate, you need to perform following steps:
    1, Get the DP that the DataTemplate is assigned to; for example ItemsControl.ItemTemplate
    2, Get the template parent which is the item container; for example itemsControl.ItemContainerGenerator.ContainerFromIndex(0)
    3, Use template find name approach such as ItemsControl.ItemTemplate.FindName() method with element name and item container parameters to find the named element inside of DataTemplate

    I appreciate your answer, but it doesn't really help me. I already understand all this theory. The problem is, the theory isn't working in practice, and the example code you gave is for a only-somewhat-similar situation. If someone could please look at the code in my original post, and tell me what specifically in it is wrong, that would be great.
    Wednesday, November 4, 2009 5:54 AM
  • Why don't you directly use the TabItem.ContentTemplate?

    tabCabinet_New.ApplyTemplate();
    DataTemplate templTab_Cabinet = tabCabinet_New.ContentTemplate;
    TextBlock txtCup = (TextBlock)(templTab_Cabinet.FindName("txtCup", objContentPresenter));


    Just a general comment: it's better to bind to a DP then to retrieve the user control (in this case txtCup) and set its value programmatically.

    Geert van Horrik - CatenaLogic
    Visit my blog: http://blog.catenalogic.com

    Looking for a way to deploy your updates to all your clients? Try Updater!
    Wednesday, November 4, 2009 9:48 AM
  • Hi,

    I tried to run your code; unfortunately I didn't have the time to complete the test to make it work, but it seems that there is a second ContentPresenter in tab_Cabinet which is bound to the Header of the control, and FindVisualChild is returning this instead of the one you want. Then of course its ContentTemplate doesn't contain the control you are looking for...

    Doing what Geert suggested would probably solve the problem.
    Wednesday, November 4, 2009 2:08 PM
  • Why don't you directly use the TabItem.ContentTemplate
    tabCabinet_New.ApplyTemplate();
    DataTemplate templTab_Cabinet = tabCabinet_New.ContentTemplate;
    TextBlock txtCup = (TextBlock)(templTab_Cabinet.FindName("txtCup", objContentPresenter));
    
    Thanks, but read my original post more carefully, specifically the parts below the code sections: I mentioned that I already tried directly using TabItem.ContentTemplate as an alternative to the code I posted, but that this just results in an InvalidOperationException rather than the NullReferenceException of the first version of the code I posted.
    Just a general comment: it's better to bind to a DP then to retrieve the user control (in this case txtCup) and set its value programmatically.
    I know, and I would have preferred to just bind the data if it was possible, but the code I gave is a much simpler, much pared-down sample of a FAR more complicated project that requires me to do things with the child elements which aren't possible with databinding.
    Wednesday, November 4, 2009 6:17 PM
  • Hi,

    I tried to run your code; unfortunately I didn't have the time to complete the test to make it work, but it seems that there is a second ContentPresenter in tab_Cabinet which is bound to the Header of the control, and FindVisualChild is returning this instead of the one you want. Then of course its ContentTemplate doesn't contain the control you are looking for...

    Doing what Geert suggested would probably solve the problem.

    What Geert suggested doesn't work, but Hbarck, your idea sounds possible. Any idea how to get the second ContentPresenter rather than the first?

    On the other hand, I watched the recursive FindVisualChild() while it was running. It looks like (based on the value returned by VisualTreeHelper.GetChildrenCount()) that tabCabinet_New only has a single child, of type ClassicBorderDecorater. ClassicBorderDecorator in turn has a single child, the ContentPresenter that is being found. There appear to be no other branches of the visual tree, so it doesn't look like there's a second ContentPresenter to *be* found, even if we were to modify FindVisualChild() to search for a second ContentPresenter rather than a first.
    Wednesday, November 4, 2009 8:50 PM
  • Please, can someone help me? It's been two weeks without anyone replying; no one has been able to offer a working fix for the code I posted. It seems like a fairly simple problem. I really need this. Thanks so much.
    Monday, November 16, 2009 6:27 AM
  • Recommended databound approach:

    <TabControl Grid.Row="0" ItemsSource="{Binding ElementName=wndMain, Path=Cabinets}">
    <TabControl.ContentTemplate>
    <DataTemplate DataType="{x:Type src:Cabinet}">
    <TextBlock Text="{Binding CupName}"/>
    </DataTemplate>
    </TabControl.ContentTemplate>
    </TabControl>

    To get this to work:
    1.  Add x:Name="wndMain" to your Window element
    2. Add an ObservableCollection<Cabinet> property named 'Cabinets' to your wndMain class
    3. Add a string property named 'CupName' to your cabinet class

    Non-databound route:
    Drop the data template and just have that TextBlock sitting in resources.  When you go to add an item to your tab control:
    1. Create the tab item
    2. Retrieve the textblock from resources (or create it in code)
    3. Add it to the TabItem's 'Content' property
    4. Add the TabItem to the TabControl's Items property

    Monday, November 16, 2009 7:35 AM
  • Here is a solution for your problem.

            private void btnAddNewCabinet_Click(object sender, RoutedEventArgs e)
            {
                // Create cabinet tab
                TabItem tabCabinet_New = new TabItem();
                tabCabinet_New.Content = new Cabinet();
                // Set the HeaderTemplate /* Sreeram: i'll explain later*/
                //tabCabinet_New.ContentTemplate = (DataTemplate)ctrlTab_Room.FindResource("templTab_Cabinet");
                tabCabinet_New.HeaderTemplate = (DataTemplate)ctrlTab_Room.FindResource("templTab_Cabinet");
    
                // Sreeram: Let the TabItem load and then do the extra stuff.
                tabCabinet_New.Loaded += new RoutedEventHandler(tabCabinet_New_Loaded);
    
                // Get cup in cabinet
                tabCabinet_New.ApplyTemplate();
                // Add cabinet to room
                ctrlTab_Room.Items.Add(tabCabinet_New);
            }
         


            void tabCabinet_New_Loaded(object sender, RoutedEventArgs e)
            {
                ContentPresenter objContentPresenter = FindVisualChild<ContentPresenter>(e.Source as DependencyObject);
                DataTemplate templTab_Cabinet = objContentPresenter.ContentTemplate;
                TextBlock txtCup = (TextBlock)(templTab_Cabinet.FindName("txtCup", objContentPresenter));
                txtCup.Text = "foobar";
            }
      Below code stub retrievs the HeaderTemplate (which is null in your code) , not the ContentTemplate. 

    ContentPresenter objContentPresenter = FindVisualChild<ContentPresenter>(e.Source as DependencyObject);
     DataTemplate templTab_Cabinet = objContentPresenter.ContentTemplate;

    There should be a way to get the ContentTemplate, but i am not sure.

    Hope this helps.


    Tuesday, November 17, 2009 7:00 AM
  • Sreeram Pavan, thanks for the reply. I don't understand why at the beginning of your code you assign the DataTemplate to the HeaderTemplate property rather than the ContentTemplate property (especially since later in your code you work with the ContentTemplate, not the HeaderTemplate property). That results in the DataTemplate being assigned to the tab of the TabItem rather than the page of the TabItem, and is not what I'm trying to do.

    I tried to follow what you suggested (moving some of the code out into a Loaded event handler) and wrote the following code:


    private void btnAddNewCabinet_Click(object objSender, RoutedEventArgs args)
    {
    	// Create cabinet tab
    	TabItem tabCabinet_New = new TabItem();
    	tabCabinet_New.ContentTemplate = (DataTemplate)ctrlTab_Room.FindResource("templTab_Cabinet");
    	tabCabinet_New.Content = new Cabinet();
    	
    	// Let the TabItem load and then do the extra stuff, including getting cup in cabinet
    	tabCabinet_New.Loaded += new RoutedEventHandler(tabCabinet_New_Loaded);
    	tabCabinet_New.ApplyTemplate();
    	
    	// Add cabinet to room
    	ctrlTab_Room.Items.Add(tabCabinet_New);
    }
    
    void tabCabinet_New_Loaded(object objSender, RoutedEventArgs args)
    {
    	ContentPresenter objContentPresenter = FindVisualChild<ContentPresenter>(args.Source as DependencyObject);
    	DataTemplate templTab_Cabinet = objContentPresenter.ContentTemplate;
    	TextBlock txtCup = (TextBlock)(templTab_Cabinet.FindName("txtCup", objContentPresenter));
    	txtCup.Text = "foobar";
    }


    but it doesn't work. It still results in a NullReferenceException in the FindName() line because templTab_Cabinet is null. I tried changing the previous line from:

    DataTemplate templTab_Cabinet = objContentPresenter.ContentTemplate;


    to:


    DataTemplate templTab_Cabinet = (args.Source as TabItem).ContentTemplate;



    but that instead results in an InvalidOperationException ("This operation is valid only on elements that have this template applied.") on the FindName() line.
    Wednesday, November 18, 2009 4:14 PM
  • Thanks, Negativë , but this is not what I'm looking for. Let me reiterate that the source I gave is a deliberately pared-down example of a much more complex project, with an extremely complicated DataTemplate containing many, many elements. I'm basically looking for a fix for the code that I posted for btnAddNewCabinet_Click(). I need to be able to retrieve child elements within a DataTemplate.

    The databound route you gave might work for binding to data inside the TextBlock, but that's not the point. I need code that lets me retrieve the TextBlock element (or any other child element) within the DataTemplate for a programmatically-created TabItem, and it should also be possible to do so without reconcepting so much of the model; I'm just looking for a fix of the code inside btnAddNewCabinet_Click().

    The non-databound route you gave doesn't work either. The whole point here is to retrieve a child element from within a DataTemplate. Creating the child element in code defeats the purpose.
    Wednesday, November 18, 2009 4:16 PM
  • Hi,

    I just found out that the body of the tab is actually in the ControlTemplate of the TabControl itself, not the TabItem. Search for TabControl ControlTemplate Example in the WPF SDK documentation, and you can see its structure. The DataTemplate you are looking for is probably applied to the ContentPresenter called "PART_SelectedContentHost".
    http://wpfglue.wordpress.com
    Monday, November 23, 2009 7:35 PM
  • Hbarck, thanks for taking the time to come back to this thread. I found the documentation you mentioned at http://msdn.microsoft.com/en-us/library/ms754137.aspx . However, I don't think the interpretation that the DataTemplate gets applied to the TabControl is correct.

    Within btnAddNewCabinet_Click(), the line


    tabCabinet_New.ContentTemplate = (DataTemplate)ctrlTab_Room.FindResource("templTab_Cabinet");



    explicitly applies the DataTemplate to the ContentTemplate property of the TabItem, not the TabControl (and at the time that above statement is executed, the TabItem has not even been bound to any TabControl yet). Applying the DataTemplate to the TabControl wouldn't even make sense (if you had multiple tabs which are each to be templated differently, then each DataTemplate would obviously need to be applied to each separate TabItem). I think applying the DataTemplate to the TabControl itself only changes the background of the TabControl (that is, how it displays when the control has no TabItems).

    Perhaps I'm misunderstanding what you're saying. If I am, could you please show me how to change the code within the btnAddNewCabinet_Click() function so that it will work? Thanks.
    Wednesday, November 25, 2009 7:59 AM
  • Hi,

    well, I think the TabControl's trick is to store the content and the ContentTemplate in the TabItems, but to assign them to a central ContentPresenter in the TabControl when the TabItem is selected.

    Anyway, this code works if you put it into the Add button's Click handler:

                TabItem tabCabinet_New = new TabItem();
                Cabinet content = new Cabinet();
                DataTemplate myTemplate = (DataTemplate)ctrlTab_Room.FindResource("templTab_Cabinet");
                tabCabinet_New.ContentTemplate = myTemplate;
                tabCabinet_New.Content = content;
                tabCabinet_New.Header = "tabCabinet";
    
                // Add cabinet to room
                ctrlTab_Room.Items.Add(tabCabinet_New);
                ctrlTab_Room.SelectedItem = tabCabinet_New;
                if (ctrlTab_Room.SelectedContent == content)
                {
                    System.Windows.MessageBox.Show("Found Content!");
                }
    
                // Get cup in cabinet
                //tabCabinet_New.ApplyTemplate();
                ContentPresenter objContentPresenter = FindVisualChild<ContentPresenter>(ctrlTab_Room);
                DataTemplate templTab_Cabinet = objContentPresenter.ContentTemplate;
                if (myTemplate == templTab_Cabinet)
                {
                    System.Windows.MessageBox.Show("Found Template");
                }
                TextBlock txtCup = (TextBlock)(templTab_Cabinet.
                    FindName("txtCup", objContentPresenter));
                txtCup.Text = "foobar";
    
    

    http://wpfglue.wordpress.com
    Wednesday, November 25, 2009 5:16 PM

  • hbarck, I hope you had a good Thanksgiving holiday, and thank you so much for this. This issue has been causing me a great deal of trouble, and thanks to you, we've now come much closer to getting things to work right.

    I tried your code, and unfortunately, while it appears to work correctly on the surface, it also contains a non-obvious bug: the DataTemplate gets "lost" whenever we switch to another tab. It's not obvious that this happens because in the above code, all created tabs are identical (they all contain the word "foobar"), but I can show that this is happening in two ways:

    (1) Try placing a TabItem into the XAML. For example, replace:


            <TabControl Grid.Row="0" Name="ctrlTab_Room">
                <TabControl.Resources>
                    <DataTemplate x:Key="templTab_Cabinet">
                        <TextBlock Name="txtCup"/>
                    </DataTemplate>
                </TabControl.Resources>
            </TabControl>



    in the original XAML with (the only thing we're doing is inserting a TabItem):


            <TabControl Grid.Row="0" Name="ctrlTab_Room">
                <TabControl.Resources>
                    <DataTemplate x:Key="templTab_Cabinet">
                        <TextBlock Name="txtCup"/>
                    </DataTemplate>
                </TabControl.Resources>
                <TabItem Header="XAML Tab">
                    <TextBlock Text="XAML Cup"/>
                </TabItem>
            </TabControl>



    Now compile and run the application. You will see a tab with the header "XAML Tab" and the content "XAML Cup". Click the "Add New Cabinet" button. A new tab is created, with the word "foobar" in its content. Now select the "XAML Tab" tab with a mouse-click. So far so good...but if we then go back to (select) the newly-created "tabCabinet" tab, we get a tab with blank content instead of the word "foobar".

    (2) An alternative test that shows that the content of the tabs are being "lost": add a counter variable to the class wndMain:


        public partial class wndMain : Window
        {
            private int nCounterTab = 0;
    
            [...]



    And now, instead of making the content of all tabs "foobar", number that content. Basically, comment out the line:


            txtCup.Text = "foobar";



    and put in its place:


            nCounterTab++;
            txtCup.Text = "foobar #" + nCounterTab;



    Now, compile and run the application. Click the "Add New Cabinet" button, and a new tab appears with the content text "foobar #1". Click the "Add New Cabinet" button again, and another new tab appears, but this time with the content text "foobar #2". Now, reselect the previous tab. Instead of displaying "foobar #1" like it should, the content has been "lost" and it displays "foobar #2", almost as if the new tab has overwritten the previous one.

    Anyone have any ideas how to fix this and prevent the content of the tabs from being "lost"? Thanks.
    Sunday, November 29, 2009 3:43 AM
  • Hi,

    this is no bug but expected behaviour. As I wrote before: The TabControl doesn't save the contents of the Tabs, it just displays it. So, every time you select another TabItem, the SelectedContent and SelectedContentTemplate are replaced by the TabItems content and ContentTemplate.

    It seems to me that you have not quite understood how a DataTemplate works. Actually, the job of a DataTemplate is to define a group of controls WPF should use to display a certain data item (which is normally not a visual object at all). This group of controls is created every time a control tries to display the data item using the template. When these controls go out of sight, they are released, and a new set of the same controls is created again when the data item is displayed again (well, at least in principle; some controls reuse these generated control sets in order to save cpu time and memory; this seems to happen in the second example). So, if you want to put some information onto a tab, don't put it into the DataTemplate, but into the data item, and bind the property of the control in the DataTemplate to the property of the object which is the data item. In your example, that would mean that you give your cabinet class a cup property and set this to foobar. Then you'd write <TextBlock x:Name="cup" Text="{Binding cup}"/> into your DataTemplate. And voila, as long as you use the same cabinet object as content, the Tab will retain "foobar" as its text...

    If you insist on modifying the controls instead of using the DataTemplate as it is intended, you could place the controls into the content of the TabItems directly, without using a DataTemplate. You could define the structure (the content of the template, so to say) as a resource, set the x:Shared attribute to false on it, and refer to it whenever you create a new TabItem.

    I'd respectfully suggest that you read again the chapter on data binding in the WPF documentation. While using DataTemplates as you are trying to do is possible, it runs against their intended use, and you probably won't have much fun with it...

    Season's Greetings,

    Hans
    http://wpfglue.wordpress.com
    Sunday, November 29, 2009 2:26 PM