2

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

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286

1 Answers1

2

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";
}
CodeNaked
  • 40,753
  • 6
  • 122
  • 148
  • Aaah! Didn't know about the OnContentTemplateChanged override. That should do the trick. One thing, in the code example in your link, for a listbox it says 'IsSynchronizedWithCurrentItem set to True for this to work' with the line of code 'ListBoxItem myListBoxItem = (ListBoxItem)(myListBox.ItemContainerGenerator.ContainerFromItem(myListBox.Items.CurrentItem));' Do you know why they just didn't use 'myListBox.SelectedItem'? Then I don't think you'd have to use 'CurrentItem', correct? Or am I missing something? – Mark A. Donohoe Apr 15 '11 at 15:17
  • @MarqueIV - Yeah, seems like SelectedItem would be the same value as Items.CurrentItem, but you'd have to test that. If you are not using a ListBox though, then you don't need to do that. Looks like you are using a TextBlock, is that correct? It should be a ContentControl, no? – CodeNaked Apr 15 '11 at 15:20
  • Odd thing I noticed... when IsEditing is false (i.e. I haven't swapped in the alternate template yet) ContentTemplate returns 'null'. I thought it would at least pick up the default 'template' for the UserControl (i.e. the main elements in the USerControl) but that doesn't seem to be the case. So looks like I'm going to have to check if ContentTemplate is null and if not, use that, but if so, go to the regular COntrol.FindName method. That's just a guess, but I have to test. (BTW, that's why I'm holding off on marking this as accepted. I'm technically not quite there yet.) – Mark A. Donohoe Apr 15 '11 at 15:58
  • @MarqueIV - No problem. I don't think the ContentTemplate is set by default. Normally, implicit DataTemplates are used in that case (i.e. a DataTemplate with just a TargetType, but no key). – CodeNaked Apr 15 '11 at 16:05
  • Still hitting issues. I put in the OnContentTemplateChanged override and called 'var textBox = newContentTemplate.FindName("EditTextBox", this);' and got an error that you can only call that on controls that have that template applied. But in the immediate window I did '? newContentTemplate == this.ContentTemplate' and it returned true! WTF?!! – Mark A. Donohoe Apr 15 '11 at 16:14
  • Even more crazy... 'var textBox = this.ContentTemplate.FindName("EditTextBox", this);' throws 'This operation is valid only on elements that have this template applied.' Gonna post this in its own question. – Mark A. Donohoe Apr 15 '11 at 16:16
  • @MarqueIV - You need to pass the ContentPresenter that is used by the UserControl to present the content. In the FindName documentation, you can see they use the FindVisual to get the associated ContentPresenter, which you'd need to do. If that doesn't help, please update your question with the code you have. – CodeNaked Apr 15 '11 at 16:17
  • Still throwing the exception, even when I'm using the VindVisualChild to get the ContentPresenter, which is still confusing since this is a UserControl where you don't define one. (Still, it did find one so it must be an implicit template for a USerControl.) But even when I call the code on the found content presenter, I still get the exact error! I've gone ahead and posted a new question on this. – Mark A. Donohoe Apr 15 '11 at 16:31
  • @MarqueIV - Ok, I can try to take a look at the other question later, if no one answers. – CodeNaked Apr 15 '11 at 16:34
  • Can you post source code for the answer you're suggesting using my faux control above? Perhaps I'm missing something. All I know is I'm banging my head against the table here and it's getting really frustrating. (I do have an ugly, hackish work around, but it's better than an exception. I set the class-level 'textBox' variable using the sender from the 'TextBox_Loaded' event. I then simply wire up the Loaded event from any of the controls in the default template and when they fire, I clear the class variable. Like I said, ugly, but it works. – Mark A. Donohoe Apr 15 '11 at 17:23