Find TextBox in a DataTemplate

3.5k Views Asked by At

Have some elements in a DataTemplate that I need to access in a code behind event handler.
Below works if the button and the textbox have the same Parent.
How would I handle a more complex layout?
If there a general way for accessing an element in a DataTemplate?

XAML

<DataTemplate x:Key="fieldDateTemplate">
    <StackPanel>
        <DatePicker SelectedDate="{Binding Path=FieldValue}" />
        <TextBox x:Name="tbFindMe" Text="findME"/>
        <Button Content="FindTB" Click="FindTB_click" Width="60" HorizontalAlignment="Left"/>
    </StackPanel>
</DataTemplate>

C#

private void FindTB_click(object sender, RoutedEventArgs e)
{
    Button btn = (Button)sender;
    TextBox tb = ((StackPanel)btn.Parent).FindName("tbFindMe") as TextBox;
}

Had to make an update to the excellent answer provided by dkozl
It was failing if there was a ListBox or ListView in the template as it would stop there
This is the fix that is working for now

private DataTemplate FieldTemplateDetail2(object sender, out ContentPresenter cp)
{
    cp = null;
    if (sender == null) return null;
    var d = sender as DependencyObject;
    DependencyObject dNext = null;
    DataTemplate template = null;
    while (d != null)
    {
        if (d is ContentPresenter)
        {
            Debug.WriteLine("FieldTemplateDetail2 d is ContentPresenter" + d.ToString());
            cp = d as ContentPresenter;
        }

        dNext = VisualTreeHelper.GetParent(d);
        if (dNext != null &&  dNext is ListBoxItem)
        {
            Debug.WriteLine("FieldTemplateDetail2 dNext is ListBoxItem " + d.ToString());
            if (cp != null)
            {
                Debug.WriteLine("FieldTemplateDetail2 cp != null" + cp.ToString());
                cp = cp.ContentTemplate.FindName("fieldTemplateDetail", cp) as ContentPresenter;
                if (cp != null)
                {
                    Debug.WriteLine("FieldTemplateDetail2 cp fieldTemplateDetail != null" + cp.ToString());
                    template = cp.ContentTemplate;
                    if (template == null && cp.ContentTemplateSelector != null)
                        template = cp.ContentTemplateSelector.SelectTemplate(cp.Content, cp);
                    break;
                }
                cp = null;
            }
        }
        //d = VisualTreeHelper.GetParent(d);
        d = dNext;
    }
    return template;
}

Based on All code below this appears to be a fix based on answer from dkozl
With the tb burried in a Grid in an Expandar it was more difficult than the simple example above

var d = sender as DependencyObject;
ContentPresenter cp = null;
while (d != null && !(d is ListBoxItem))
{
    if (d is ContentPresenter) cp = d as ContentPresenter;
    d = VisualTreeHelper.GetParent(d);
}
if (cp != null) cp = cp.ContentTemplate.FindName("fieldTemplateDetail", cp) as ContentPresenter;
if (cp != null)
{               
    var template = cp.ContentTemplate;
    if (template == null && cp.ContentTemplateSelector != null)
        template = cp.ContentTemplateSelector.SelectTemplate(cp.Content, cp);
    if (template != null)
    {
        var tb = template.FindName("tbFindMe", cp) as TextBox;
        if (tb == null) MessageBox.Show("null", "ContentTemplateSelector");
        else  MessageBox.Show(tb.Text, "ContentTemplateSelector");
    }
}

All code as requested

<Window x:Class="ListViewTemplateSelectorWPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:ListViewTemplateSelectorWPF"
        DataContext="{Binding RelativeSource={RelativeSource self}}"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <BooleanToVisibilityConverter x:Key="bvc" />
        <local:FieldTemplateSelector x:Key="fieldTemplateSelector"/>
        <DataTemplate x:Key="windowTemplate">
            <TextBox x:Name="windowTemplateTB" Text="windowTemplate" />
        </DataTemplate>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <ListBox Grid.Row="0" x:Name="lbFields"
                 ItemsSource="{Binding Path=Fields}"
                 HorizontalContentAlignment="Stretch">
            <ListBox.Resources>
                <DataTemplate x:Key="fieldStringTemplate">
                    <StackPanel x:Name="fieldString" Visibility="Visible">
                        <TextBox Text="{Binding Path=FieldValue}" />
                    </StackPanel>
                </DataTemplate>
                <DataTemplate x:Key="fieldDateTemplate">
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="auto"/>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                        </Grid.RowDefinitions>
                        <DatePicker Grid.Row="0" SelectedDate="{Binding Path=FieldValue}" />
                        <!--<TextBox Grid.Row="1" x:Name="tbFindMe" Text="findME"/>
                        <Button Grid.Row="2" Content="FindTB" Click="FindTB_click" Width="60" HorizontalAlignment="Left"/>-->
                        <Expander Grid.Row="1" Header="Find">
                            <Grid>
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="auto"/>
                                    <RowDefinition Height="Auto"/>
                                </Grid.RowDefinitions>
                                <TextBox Grid.Row="0" x:Name="tbFindMe" Text="findME"/>
                                <Button Grid.Row="1" Content="FindTB" Click="FindTB_click" Width="60" HorizontalAlignment="Left"/>
                            </Grid>
                        </Expander>
                    </Grid>
                </DataTemplate>
            </ListBox.Resources>
            <ListBox.ItemTemplate>
                <DataTemplate DataType="local:Field">
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="60"/>
                            <ColumnDefinition Width="*"/>
                        </Grid.ColumnDefinitions>
                        <TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding Path=Name}" />
                        <TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Path=DisplayValue}" />                   
                        <ContentPresenter Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
                                    Visibility="{Binding RelativeSource={RelativeSource AncestorType=ListBoxItem},
                                                         Path=IsSelected, Converter={StaticResource bvc}}"
                                    x:Name="fieldTemplateDetail"
                                    Content="{Binding}"
                                    ContentTemplateSelector="{StaticResource fieldTemplateSelector}"/>                   
                    </Grid>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <Button Grid.Row="1" x:Name="findButton" Content="Waste Button" Width="100" HorizontalAlignment="Left" Click="click_Unselect"/>
    </Grid>
</Window>

using System.ComponentModel;
namespace ListViewTemplateSelectorWPF
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    /// 

    public partial class MainWindow : Window
    {
        private List<Field> fields = new List<Field>();
        public MainWindow()
        {
            fields.Add(new FieldString("String1"));
            fields.Add(new FieldString("String2"));
            fields.Add(new FieldDate("Date1"));
            fields.Add(new FieldDate("Date2"));

            InitializeComponent();          
        }
        public Field CurField { get; set; }
        public List<Field> Fields { get { return fields; } }

        private void click_Unselect(object sender, RoutedEventArgs e)
        {            
            try
            {
                Button tb = this.FindName("findButton") as Button;
                if (tb == null) MessageBox.Show("null");
                else MessageBox.Show(tb.Name);
            }
            catch (Exception Ex)
            {
                MessageBox.Show(Ex.Message, "exception findButton");
            }
            try
            {
                DataTemplate dt = this.FindResource("fieldDateTemplate") as DataTemplate;
                if (dt == null) MessageBox.Show("dt not found");
                else MessageBox.Show("dt found");
            }
            catch (Exception Ex)
            {
                MessageBox.Show(Ex.Message, "exception dt");
            }

            lbFields.SelectedIndex = -1;
        }

        private void FindTB_click(object sender, RoutedEventArgs e)
        {
            var d = sender as DependencyObject;
            while (d != null && !(d is ContentPresenter)) d = VisualTreeHelper.GetParent(d);
            var cp = d as ContentPresenter;
            if (cp != null)
            {
                var template = cp.ContentTemplate;
                if (template == null && cp.ContentTemplateSelector != null)
                    template = cp.ContentTemplateSelector.SelectTemplate(cp.Content, cp);
                if (template != null)
                {
                    var tb = template.FindName("tbFindMe", cp) as TextBox;
                    MessageBox.Show(tb.Text, "ContentTemplateSelector");
                }
            }

            Button btn = (Button)sender;
            //MessageBox.Show("button name = " + btn.Name);
            try
            {
                TextBox tb = ((Grid)btn.Parent).FindName("tbFindMe") as TextBox;
                if (tb == null) MessageBox.Show("null","manual");
                else MessageBox.Show(tb.Text, "manual");
            }
            catch (Exception Ex)
            {
                MessageBox.Show(Ex.Message, "exception manual");
            }
        }
    }
    public abstract class Field : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void NotifyPropertyChanged(String info)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(info));
            }
        }
        private string name;
        public string Name { get { return name; } }
        public abstract string DisplayValue { get; }
        public Field(string Name) { name = Name; }
    }
    public class FieldString : Field
    {
        private string fieldValue;
        public string FieldValue
        {
            get { return fieldValue; }
            set
            {
                if (fieldValue == value) return;
                fieldValue = value;
                NotifyPropertyChanged("FieldValue");
                NotifyPropertyChanged("DisplayValue");
            }
        }
        public override string DisplayValue
        {
            get { return FieldValue; }
        }
        public FieldString(string Name) : base(Name) { }
        public FieldString(string Name, string FieldValue) : base(Name)
        {   fieldValue = FieldValue; } 
    }
    public class FieldDate : Field
    {
        private  DateTime? fieldValue = null;
        public DateTime? FieldValue
        {
            get { return fieldValue; }
            set
            {
                if (fieldValue == value) return;
                fieldValue = value;
                NotifyPropertyChanged("FieldValue");
                NotifyPropertyChanged("DisplayValue");
            }
        }
        public override string DisplayValue
        {
            get { return FieldValue.ToString(); }
        }
        public FieldDate(string Name) 
            : base(Name) { }
        public FieldDate(string Name, DateTime FieldValue)
            : base(Name)
        { fieldValue = FieldValue; }
    }
    public class FieldTemplateSelector : DataTemplateSelector
    {
        public override DataTemplate
            SelectTemplate(object item, DependencyObject container)
        {
            FrameworkElement element = container as FrameworkElement;
            if (item != null && item is Field)
            {
                System.Diagnostics.Debug.WriteLine("Field");
                if (item is FieldString)
                {
                    System.Diagnostics.Debug.WriteLine("FieldString");
                    return element.FindResource("fieldStringTemplate") as DataTemplate;
                }
                if (item is FieldDate)
                {
                    System.Diagnostics.Debug.WriteLine("FieldDate");
                    return element.FindResource("fieldDateTemplate") as DataTemplate;                }

                return element.FindResource("fieldTemplate") as DataTemplate;
            }
            else
                return element.FindResource("fieldTemplate") as DataTemplate;
        }
    }
}
2

There are 2 best solutions below

11
On BEST ANSWER

To find controls by name inside DataTemplate you need to find ContentPresenter that uses this template and call FindName on that template with found ContentPresenter:

private void Button_Click(object sender, RoutedEventArgs e)
{
   var d = sender as DependencyObject;
   ContentPresenter cp = null;
   while (d != null && !(d is ListBoxItem))
   {
       if (d is ContentPresenter) cp = d as ContentPresenter;
       d = VisualTreeHelper.GetParent(d);
   }
   if (cp != null)
   {
      cp = cp.ContentTemplate.FindName("fieldTemplateDetail", cp) as ContentPresenter;
      if (cp != null)
      {
         var template = cp.ContentTemplate;
         if (template == null && cp.ContentTemplateSelector != null)
            template = cp.ContentTemplateSelector.SelectTemplate(cp.Content, cp);
         if (template != null)
         {
            var tb = template.FindName("tbFindMe", cp) as TextBox;
         }
      }
   }
}
0
On

This older post has been a lifesaver. I have been on this problem for past few hours and dkozl provided some great insight. Hopefully my explanation will help a few others.

I have a datagrid with some columns (7 to be exact) 5 of them display static data on the item, 1 is a textbox entry/data display, and the last one is an indicator for on and off for boolean items.

I am currently improving the user experience while using the datagrid (using F2 to edit, space to toggle boolean values, and so forth)

The XAML below i will only have one column in datagrid with a textbox:

<DataGrid x:Name="my_DG" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" AutoGenerateColumns="False" KeyDown="my_DG_KeyDown">
    <DataGrid.Columns>
        <DataGridTemplateColumn>
            <DataGridTemplateColumn.Header>
                <TextBlock Text="Value" FontSize="18"/>
            </DataGridTemplateColumn.Header>
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <TextBox x:Name="txtBoxValue" Text="{Binding Path=Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" KeyDown="EventTrigger_KeyDown"/>
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
            <DataGridTemplateColumn.CellStyle>
                <Style TargetType="DataGridCell">
                    <Setter Property="IsTabStop" Value="False" />
                </Style>
            </DataGridTemplateColumn.CellStyle>
        </DataGridTemplateColumn>
    <DataGrid.Columns>
</DataGrid>

The effect i was going for is when i had a row highlighted on the datagrid when i push F2 focus would go to the textbox and the user can start editing the value. This was done with the KeyDown="my_DG_KeyDown" event. The code on the event is:

private void my_DG_KeyDown(object sender, KeyEventArgs e)
{
    if (e.Key.Equals(Key.F2))
    {
        var currRow = (sender as DataGrid).CurrentItem;
        //Columns[0] is the column the text box is in for the given row.
        var currCell_CP = (sender as DataGrid).Columns[0].GetCellContent(currRow);
        var itm = (currCell_CP as ContentPresenter).ContentTemplate.FindName("txtBoxValue", currCell_CP) as TextBox;
        itm.Focus();
    }
}

The important take away is i was able to get the ContentPresenter of a given Cell. Then from there i was able to get the template and search for the textbox name ("txtBoxValue") with those 2 items.

I find this a bit more straight forward than dkozl answer, but i wouldn't have came to this without his help