Search code examples
wpftemplatestriggersfindcontrolcontenttemplateselector

In WPF, how do I find an element in a template that's switched in via a trigger?


I have a UserControl (not a lookless custom control) which, depending on some custom state properties, swaps in various ContentTemplates, all defined as resources in the associated XAML file. In the code-behind, I need to find one of the elements in the swapped-in ContentTemplates.

Now in a lookless control (i.e. a custom control), you simply override OnApplyTemplate then use FindName, but that override doesn't fire when the ContentTemplate gets switched by a trigger (...at least not for a UserControl. I haven't tested that functionality with a custom control.)

Now I've tried wiring up the Loaded event to the control in the swapped-in template, which does fire in the code-behind, then I simply store 'sender' in a class-level variable. However, when I try to clear that value by subscribing to the Unloaded event, that doesn't fire either because the tempalte gets swapped out, thus unwiring that event before it has a chance to be called and the control unloads from the screen silently, but I still have that hung reference in the code-behind.

To simulate the OnApplyTemplate functionality, I'm considering subscribing to the ContentTemplateChanged notification and just using VisualTreeHelper to look for the control I want, but I'm wondering if there's a better way, hence this post.

Any ideas?

For reference, here's a very-stripped-down example of the control I have. In this example, if IsEditing is true, I want to find the textbox named 'FindMe'. If IsEditing is false which means the ContentTemplate isn't swapped in, I want to get 'null'...

<UserControl x:Class="Crestron.Tools.ProgramDesigner.Controls.EditableTextBlock"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:Crestron.Tools.ProgramDesigner.Controls"
    x:Name="Root">

    <UserControl.Resources>

        <DataTemplate x:Key="EditModeTemplate">

            <TextBox x:Name="FindMe"
                Text="{Binding Text, ElementName=Root}" />

        </DataTemplate>

        <Style TargetType="{x:Type local:EditableTextBlock}">
            <Style.Triggers>

                <Trigger Property="IsEditing" Value="True">
                    <Setter Property="ContentTemplate" Value="{StaticResource EditModeTemplate}" />
                </Trigger>

            </Style.Triggers>
        </Style>

    </UserControl.Resources>

    <TextBlock x:Name="TextBlock"
        Text="{Binding Text, ElementName=Root}" />

</UserControl>

Aaaaaaand GO!

M


Solution

  • Unfortunately, there isn't a better way. You can override the OnContentTemplateChanged, instead of hooking up to the event.

    You would need to use the DataTemplate.FindName method to get the actual element. The link has an example of how that method is used.

    You would need to delay the call to FindName if using OnContentTemplateChanged though, as it is not applied to the underlying ContentPresenter immediately. Something like:

    protected override void OnContentTemplateChanged(DataTemplate oldContentTemplate, DataTemplate newContentTemplate) {
        base.OnContentTemplateChanged(oldContentTemplate, newContentTemplate);
    
        this.Dispatcher.BeginInvoke((Action)(() => {
            var cp = FindVisualChild<ContentPresenter>(this);
            var textBox = this.ContentTemplate.FindName("EditTextBox", cp) as TextBox;
            textBox.Text = "Found in OnContentTemplateChanged";
        }), DispatcherPriority.DataBind);
    }
    

    Alternatively, you may be able to attach a handler to the LayoutUpdated event of the UserControl, but this may fire more often than you want. This would also handle the cases of implicit DataTemplates though.

    Something like this:

    public UserControl1() {
        InitializeComponent();
        this.LayoutUpdated += new EventHandler(UserControl1_LayoutUpdated);
    }
    
    void UserControl1_LayoutUpdated(object sender, EventArgs e) {
        var cp = FindVisualChild<ContentPresenter>(this);
        var textBox = this.ContentTemplate.FindName("EditTextBox", cp) as TextBox;
        textBox.Text = "Found in UserControl1_LayoutUpdated";
    }