locked
Finding child controls in a listview item. RRS feed

  • Question

  • Hi all,

    I'm having the mother of all problems, and have been for most of a day now. Badically all I want to do is get a reference to a child control in a data bound listview item so I can validate and extract its contents, but I'm having no luck at all.

    A bit more info: I have aListView control on my page, but the template is selected dynamically using a DataTemplateSelector. The aim is to create a ListView with a different control type on each row (either a checkbox, textbox, or DatePicker) based on the type of data which needs to be input.

    All well and good so far: my TemplateSelector works as expected, and the correct controls are displayed on each row. Here is the ListView markup:

    <ListView x:Name="lvParameters">
      <ListView.View>
        <GridView>
          <GridViewColumn Header="Name" Width="200" DisplayMemberBinding="{Binding Path=DisplayName}">      </GridViewColumn>
          <GridViewColumn Header="Value" Width="225" CellTemplate="{StaticResource templateString}" CellTemplateSelector="{StaticResource parameterTemplateSelector}">
          </GridViewColumn>
        </GridView>
      </ListView.View>
    </ListView>
    And each of the data templates are defined in the resources- here's an example:

    <DataTemplate x:Key="templateString">
        <TextBox x:Name="ctrlString" MaxLength="255" Validation.Error="Validation_Error">
            <TextBox.Text>
                <Binding Path="Value" Mode="TwoWay">
                </Binding>
            </TextBox.Text>
        </TextBox>
    </DataTemplate>
    Now what I want to do is get a reference to the textbox named ctrlString when the submit button is clicked. Everything I have tried fails miserably.

    This is what I currently have:

                foreach (ReportParameterView param in this.lvParameters.Items)
                {
                    // Find its container.
                    lvi = this.lvParameters.ItemContainerGenerator.ContainerFromItem(param) as ListViewItem;
    
                    // Find the Grid View Row Content Presenter.
                    GridViewRowPresenter contentPresenter = VisualTreeUtils.FindVisualChild<GridViewRowPresenter>(lvi);
    
                    // Get the column details same index as header
                    DependencyObject child = VisualTreeHelper.GetChild(contentPresenter, 1);
    
                    // Find the control
                    DataTemplate myDataTemplate = (child as ContentPresenter).ContentTemplate;
                    ctrl = myDataTemplate.FindName("ctrl" + param.DataType.ToString(), child as FrameworkElement) as Control;
    
                    //validate the control
                    if (!this.SetControlValidState(ctrl))
                        isValid = false;
                    
                }
    Where ReportParameterView is the object type my ListView is bound to. However the FindName method always returns null, even though I am passing in the correct name...

    Also, when I hover over the DataTemplate in debug mode and drill down into its VisualTree- it shows as null. This seems a bit odd to me- surely this should contain my textbox??

    Does anybody have any idea why this is happening and how I can fix it?

    Thanks in advance,

    Adam
    Tuesday, June 2, 2009 11:20 AM

Answers

  • Well, I finally managed to get it to work... I returned to it this afternoon after a break of a couple of days and decided to approach the problem from a different direction.

    Since I couldn't find the control by recursing down the visual tree from the listview item, I thought I would try getting a reference to the control, then recursing back up the tree to the listview item, then storing a reference to the control as the ListViewItem's tag property value.

    So I started off by wiring up the Loaded event for all the controls in the data templates. i.e.

            <DataTemplate x:Key="templateString">
                <TextBox x:Name="ctrlString" MaxLength="255" Validation.Error="Validation_Error" Loaded="ctrl_Loaded">
                    <TextBox.Text>
                        <Binding Path="Value" Mode="TwoWay">
                        </Binding>
                    </TextBox.Text>
                </TextBox>
            </DataTemplate>
    And then in the Loaded event handler, I now have this:

            private void ctrl_Loaded(object sender, RoutedEventArgs e)
            {
                Control ctrl = sender as Control;
                ContentPresenter cp = (ContentPresenter)ctrl.TemplatedParent;
                ReportParameterView param = (ReportParameterView)cp.Content;
                ListViewItem lvi = (ListViewItem)this.lvParameters.ItemContainerGenerator.ContainerFromItem(param);
                lvi.Tag = ctrl;
            }
    So, as each of the controls are loaded, I set the tag of their parent ListViewItem, which then allows me to iterate over the items and get a reference to each of the child controls when I need to validate.

    This seems to have fixed my problem- but this does seem to be a fundamental problem with the way DataTemplateSelectors work IMO.
    • Marked as answer by booler Friday, June 5, 2009 2:36 PM
    Friday, June 5, 2009 2:36 PM

All replies

  • My 1 cent

    - Try disabling Virtualization on the Listview and see if it is the issue. If so, then you can see what you can do without disabling the virtualization.
    Software Engineer 1, http://krishnabhargav.blogspot.com
    Tuesday, June 2, 2009 12:43 PM
  • Hi, thanks for the response. I tried setting the virtualization mode to 'standard', but the FindName method still returns null :(
    Tuesday, June 2, 2009 1:02 PM
  • You can use VisualTreeHelper to help you to find the control.

    I build a simple demo project.

    In this demo project, there is a ListView with DataTemplate. There are some TextBlock in DataTemplate.
    But there is only one TextBlock name "tb001".

    ItemContainerGenerator is used to get the SelectedItem. VisualTreeHelper is used to find the named TextBlock in DataTemplate.

    Run the program and change selection, the TextBlock could be found and a MessageBox will be shown.

    Hope it helps.

    XAML
    <Window x:Class="WpfApplication10.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:local="clr-namespace:WpfApplication10"
        Title="Window1" Height="300" Width="500">
        <Window.Resources>
            <ObjectDataProvider x:Key="EmployeeInfoDataSource"
               ObjectType="{x:Type local:myEmployees}"/>
        </Window.Resources>
        <StackPanel>
            <ListView x:Name="MyListView"
                      SelectionChanged="MyListView_SelectionChanged"
                ItemsSource="{Binding 
                Source={StaticResource EmployeeInfoDataSource}}"  >
                <ListView.View>
                    <GridView  AllowsColumnReorder="true"
                        ColumnHeaderToolTip="Employee Information">
                        <GridViewColumn  x:Name="gridClm_SelectRow" Width="35">
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <Border BorderBrush="#FF000000" BorderThickness="1,1,0,0" Margin="-6,-2,-6,-2">
                                        <CheckBox Margin="0" x:Name="cbSelectRow" 
                                              IsChecked="{Binding 
                                        RelativeSource={RelativeSource FindAncestor,
                                        AncestorType={x:Type ListViewItem}}, 
                                        Path=IsSelected}"                                 
                                              />
                                    </Border>
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
                            <CheckBox IsEnabled="False" Margin="0" x:Name="chkSelectAll"  />
                        </GridViewColumn>
                        <GridViewColumn  
                              Width="100">
                            <GridViewColumnHeader Content="Title" IsHitTestVisible="False" />
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <Border BorderBrush="#FF000FF0" BorderThickness="2,2,0,0" >
                                        <StackPanel Width="100" >
                                            <TextBlock  Text="{Binding FirstName}"/>
                                        </StackPanel>
                                    </Border>
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
                        </GridViewColumn>
                        <GridViewColumn 
                                Header="Last Name" Width="100">
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <Border BorderBrush="#FF000FF0" BorderThickness="2,2,0,0" >
                                        <StackPanel Width="100" >
                                            <TextBlock Name="tb001" Text="{Binding LastName}"/>
                                        </StackPanel>
                                    </Border>
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
                        </GridViewColumn>
                        <GridViewColumn 
                                Header="Employee No."  
                             >
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <Border BorderBrush="#FF000FF0" BorderThickness="2,2,0,0" >
                                        <StackPanel  Width="100"    >
                                            <TextBlock Text="{Binding EmployeeNumber}"/>
                                        </StackPanel>
                                    </Border>
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
                        </GridViewColumn>
                    </GridView>
                </ListView.View>
            </ListView>
        </StackPanel>
    </Window>
    

    C#
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;
    using System.Windows.Shapes;
    using System.Collections.ObjectModel;
    namespace WpfApplication10
    {
        /// <summary>
        /// Interaction logic for Window1.xaml
        /// </summary>
        public partial class Window1 : Window
        {
            public Window1()
            {
                InitializeComponent();
            }
            private void MyListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
            {
                ItemContainerGenerator  generator = this.MyListView.ItemContainerGenerator;
                ListBoxItem selectedItem = (ListBoxItem)generator.ContainerFromIndex(MyListView.SelectedIndex);
                TextBlock tbFind = GetDescendantByType(selectedItem, typeof(TextBlock), "tb001") as TextBlock;
                if (tbFind != null)
                {
                    MessageBox.Show("find TextBlock named 'tb001': " + tbFind.Text);
                }
                else
                {
                    MessageBox.Show("can not find");
                }
            }
            public static Visual GetDescendantByType(Visual element, Type type, string name )
            { 
                if (element == null) return null;
                if (element.GetType() == type)
                {
                    FrameworkElement fe = element as FrameworkElement;
                    if (fe != null)
                    {
                        if (fe.Name == name)
                        {
                            return fe;
                        }
                    }
                } 
                Visual foundElement = null;
                if (element is FrameworkElement)
                    (element as FrameworkElement).ApplyTemplate();
                for (int i = 0;
                    i < VisualTreeHelper.GetChildrenCount(element); i++)
                {
                    Visual visual = VisualTreeHelper.GetChild(element, i) as Visual;
                    foundElement = GetDescendantByType(visual, type, name);
                    if (foundElement != null)
                        break;
                }
                return foundElement;
            }
        }
        public class EmployeeInfo
        {
            private string _firstName;
            private string _lastName;
            private string _employeeNumber;
            public string FirstName
            {
                get { return _firstName; }
                set { _firstName = value; }
            }
            public string LastName
            {
                get { return _lastName; }
                set { _lastName = value; }
            }
            public string EmployeeNumber
            {
                get { return _employeeNumber; }
                set { _employeeNumber = value; }
            }
            public EmployeeInfo(string firstname, string lastname, string empnumber)
            {
                _firstName = firstname;
                _lastName = lastname;
                _employeeNumber = empnumber;
            }
        }
        public class myEmployees :
                ObservableCollection<EmployeeInfo>
        {
            public myEmployees()
            {
                Add(new EmployeeInfo("Jesper", "Aaberg", "12345"));
                Add(new EmployeeInfo("Dominik", "Paiha", "98765"));
                Add(new EmployeeInfo("Yale", "Li", "23875"));
                Add(new EmployeeInfo("Muru", "Subramani", "49392"));
            }
        }
    }
    
    • Proposed as answer by Tao Liang Wednesday, June 3, 2009 7:28 AM
    Wednesday, June 3, 2009 7:28 AM
  • Still the same result, I'm afraid.

    I changed the code to this:

                foreach (ReportParameterView param in this.lvParameters.Items)
                {
                    //only validate required parameters
                    if (!param.IsRequired)
                        continue;
    
                    // Find its container.
                    lvi = this.lvParameters.ItemContainerGenerator.ContainerFromItem(param) as ListViewItem;
    
                    Type controlType = typeof(MyCustomControls.NumericTextBox);
                    if (param.DataType == enumReportParameterDataType.DateTime)
                        controlType = typeof(Microsoft.Windows.Controls.DatePicker);
                    else if (param.DataType == enumReportParameterDataType.Boolean)
                        controlType = typeof(CheckBox);
                    else if (param.DataType == enumReportParameterDataType.String)
                        controlType = typeof(TextBox);
    
                    Visual child = VisualTreeUtils.GetDescendantByType(lvi, controlType, "ctrl" + param.DataType.ToString());
    
                    // Find the Grid View Row Content Presenter.
                    GridViewRowPresenter contentPresenter = VisualTreeUtils.FindVisualChild<GridViewRowPresenter>(lvi);
                    contentPresenter.BringIntoView();
    
                    // Get the column details same index as header
                    //DependencyObject child = VisualTreeHelper.GetChild(contentPresenter, 1);
    
                    // Find the control
                    DataTemplate myDataTemplate = (child as ContentPresenter).ContentTemplate;
                    ctrl = myDataTemplate.FindName("ctrl" + param.DataType.ToString(), child as FrameworkElement) as Control;
    
                    //validate the control
                    if (!this.SetControlValidState(ctrl))
                        isValid = false;
                    
                }
    With the method you posted above being called via the VisualTreeUtils class- but the element still returns null.

    At the moment I can't even figure out where the problem lies, so I don't know what I should be changing to fix it. Is it caused by the fact that the data templates for each of the control types are declared in the page resources, rather than within the listview? Or is it caused by the fact that I'm using a DataTemplateSelector to select the template at runtime? Or something else entirely?

    Thanks for your suggestions so far- any others will also be much appreciated.

    Wednesday, June 3, 2009 9:14 AM
  • Hi booler,

    You can loop through the Visual Tree and find the exact element by its name or type.

    For this, use the following
    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
                {
                    Visual visual = VisualTreeHelper.GetChild(element, i) as Visual;
                   
                }
    Write this as a recursive method whose return type is a FrameworkElement. Here element is the ListViewItem in your case.
    Wednesday, June 3, 2009 9:34 AM
  • Hi indianguy- yes you're right, and this is what Tao Liang suggested above, but unfortunately, this is not finding the expected child control in the visual tree. For some reason it doesn't really find any visual tree at all. I have created a screenshot of what I see in debug mode in the hope that it will shed some light on the problem:

    GetDescendantByType in Debug Mode

    You can see in the image that I have drilled down into the content template, which was dynamically loaded by the TemplateSelector. In there, I would expect to see a visual tree, which would contain my control- however as you can (just about) see in the above image, the visual tree is null. This makes no sense to me.

    I have a label statically declared also within the gridview row- in the first column. When I loop through the visual tree I can see this label in there and can access its properties. It's just the child controls loaded by the template selector which I can't access. I'm sure there must be a workaround though- I just don't know what it is!
    Wednesday, June 3, 2009 10:23 AM
  • So really, I suppose the question is- can anybody imagine why the visual tree would be returning null for a datatemplate loaded by a DataTemplateSelector and declared in the window resources- even though the controls appear within the listview just fine?
    Thursday, June 4, 2009 9:53 AM
  • Hi booler,

    The issue is happening because you are trying to find a visual element from a DataTemplate. Instead of finding the DataTemplate, you have to find the exact element ( i.e. TextBox )from the VisualTree.
    public FrameworkElement GetChild( FrameworkElement parent, string ctrlName )
            {
                if (parent == null)
                    return null;
                if (parent.Name == ctrlName)
                    return parent;
                FrameworkElement elementFound = null;
                for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
                {
                    FrameworkElement visual = VisualTreeHelper.GetChild(parent, i) as FrameworkElement;
                    elementFound = this.GetChild(visual, ctrlName);
                    if (elementFound != null)
                        break;
                }
                return elementFound;
            }
    TextBox txtName = this.GetChild(lvi, "ctrlString")as TextBox;
                    if (txtName != null)
                    {
                        txtName.Background = Brushes.DarkCyan;
                    }
    Try the above given code.  Here lvi is the ListViewItem. This code is working fine with the DataTemplate you have given in you first thread.
    Friday, June 5, 2009 1:10 PM
  • Hi,

    Thanks again, indianguy. However, the code you posted works only so long as the DataTemplateSelector is not brought into the equation. If I specify the CellTemplate in the markup to point to a single DataTemplate in the resources, the code you posted above works fine, and retrieves the child control. However, when I add a CellTemplateSelector, and load the control type dynamically, I can no longer access the child control in the ListViewItem. Do you know why this is?
    Friday, June 5, 2009 1:52 PM
  • Well, I finally managed to get it to work... I returned to it this afternoon after a break of a couple of days and decided to approach the problem from a different direction.

    Since I couldn't find the control by recursing down the visual tree from the listview item, I thought I would try getting a reference to the control, then recursing back up the tree to the listview item, then storing a reference to the control as the ListViewItem's tag property value.

    So I started off by wiring up the Loaded event for all the controls in the data templates. i.e.

            <DataTemplate x:Key="templateString">
                <TextBox x:Name="ctrlString" MaxLength="255" Validation.Error="Validation_Error" Loaded="ctrl_Loaded">
                    <TextBox.Text>
                        <Binding Path="Value" Mode="TwoWay">
                        </Binding>
                    </TextBox.Text>
                </TextBox>
            </DataTemplate>
    And then in the Loaded event handler, I now have this:

            private void ctrl_Loaded(object sender, RoutedEventArgs e)
            {
                Control ctrl = sender as Control;
                ContentPresenter cp = (ContentPresenter)ctrl.TemplatedParent;
                ReportParameterView param = (ReportParameterView)cp.Content;
                ListViewItem lvi = (ListViewItem)this.lvParameters.ItemContainerGenerator.ContainerFromItem(param);
                lvi.Tag = ctrl;
            }
    So, as each of the controls are loaded, I set the tag of their parent ListViewItem, which then allows me to iterate over the items and get a reference to each of the child controls when I need to validate.

    This seems to have fixed my problem- but this does seem to be a fundamental problem with the way DataTemplateSelectors work IMO.
    • Marked as answer by booler Friday, June 5, 2009 2:36 PM
    Friday, June 5, 2009 2:36 PM