Why would 'this.ContentTemplate.FindName' throw an InvalidOperationException on its own template?
Asked Answered
D

3

22

Ok... this has me stumped. I've overridden OnContentTemplateChanged in my UserControl subclass. I'm checking that the value passed in for newContentTemplate does in fact equal this.ContentTemplate (it does) yet when I call this...

var textBox = this.ContentTemplate.FindName("EditTextBox", this);

...it throws the following exception...

"This operation is valid only on elements that have this template applied."

Per a commenter in another related question, he said you're supposed to pass in the content presenter for the control, not the control itself, so I then tried this...

var cp = FindVisualChild<ContentPresenter>(this);

var textBox = this.ContentTemplate.FindName("EditTextBox", cp);

...where FindVisualChild is just a helper function used in MSDN's example (see below) to find the associated content presenter. While cp is found, it too throws the same error. I'm stumped!!

Here's the helper function for reference...

private TChildItem FindVisualChild<TChildItem>(DependencyObject obj)
where TChildItem : DependencyObject {

    for(int i = 0 ; i < VisualTreeHelper.GetChildrenCount(obj) ; i++) {

        var child = VisualTreeHelper.GetChild(obj, i);

        if(child is TChildItem typedChild) {
            return typedChild;
        }
        else {
            var childOfChild = FindVisualChild<TChildItem>(child);
            if(childOfChild != null)
                return childOfChild;
        }
    }

    return null;
}
Darling answered 15/4, 2011 at 16:22 Comment(0)
Y
20

Explicitly applying the template before calling the FindName method will prevent this error.

this.ApplyTemplate(); 
Yarber answered 17/3, 2013 at 23:46 Comment(3)
Holy crap! If it's really that simple (which the docs do suggest!), I'm gonna kick myself! I guess this clarifies that the template is changed but is not applied at this point. Your call ensures it will be. Nice!!Darling
I have a question here. I am doing some changes inside ApplyTemplate() only to override some some control value.then what is the solution?Retinoscope
In my case applying the method call to the ContentPresenter (the second parameter of the call to FindName() ) did the trick, i.e. cp.ApplyTemplate()Alvinalvina
L
4

As John pointed out, the OnContentTemplateChanged is being fired before it is actually applied to the underlying ContentPresenter. So you'd need to delay your call to FindName until it is applied. 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";
}
Lechery answered 19/4, 2011 at 17:24 Comment(3)
Still don't agree with MS's naming of that function as if it hasn't yet been applied, then it really hasn't changed IMO (let alone how an almost identically named OnApplyTemplate you don't have to deal with this), but you get the 'Accept' since you gave me a code example that gives me what I want.Darling
you're always good at this kind of thing. However, isn't even your answer much more complex than @Adabyron's below, which is simply to explicitly call this.ApplyTemplate()? I have to find that old code to test, but according to the docs, that's exactly what it's there for, even telling you if the VisualTree changed as a result of the call.Darling
@MarqueIV - Yeah, that does seem like an easier solution, eh? The only benefit of mine now is that it could be adapted to handle implicitly applied DataTemplates (i.e. via implicit Style, etc).Lechery
B
0

The ContentTemplate isn't applied to the ContentPresenter until after that event. While the ContentTemplate property is set on the control at that point, it hasn't been pushed down to bindings internal to the ControlTemplate, like the ContentPresenter's ContentTemplate.

What are you ultimately trying to do with the ContentTemplate? There might be a better overall approach to reach your end goal.

Brownstone answered 15/4, 2011 at 19:12 Comment(4)
Then why on earth have that event? And it does say 'Changed' not 'Preview' or 'WillChange'. From what you said it actually hasn't yet changed. And the error is definitely misleading. (Also, again, I only tried the ContentPresenter thing per someone else. I'm using a UserControl so I'm not using a content presenter. I'm just swapping out the existing tags in the usercontrol with others via a template in the resources. As for what I'm trying to do, it's get one of the elements that's swapped in by that template, which I thought the code above would have shown.Darling
The way to achieve that that I came up with was to define a class-level variable to hold the control reference. I then attach a Loaded event to it via the template. Then in the code-behind, I simply store 'sender' in the variable. I then use the OnContentTemplateChanged call to set that variable to 'null'. It's a hacky thing, but it does work. Still, why have that above event where you're literally handed a template, but you can't do anything with it?! Makes no sense, especially since OnApplyTemplate does apply it. By that thought, this should too.Darling
You should look more closely at your visual tree at runtime using a tool like Snoop to better understand what is actually being rendered. UserControl is a ContentControl, which means it uses a ContentPresenter in its default template to display Content. The ContentTemplate is applied to that ContentPresenter through binding after it has been assigned to the ContentTemplate property on the UserControl. It's clear that you're trying to get EditTextBox, but why? Often you can get rid of the event code and use data binding instead.Brownstone
"The ContentTemplate is applied to that ContentPresenter through binding after it has been assigned to the ContentTemplate property on the UserControl." then how can I detect that change?! By your own definition (and the controls) the ContentTemplate IS set to the control and thus that binding should have already been executed, so why doesn't FindName work? Why wouldn't that binding have already been updated by the time I've already executed base.OnContentTemplateChanged? Lemme ask it a different way. How can I get the textbox immediately when the content template changes the visual tree?Darling

© 2022 - 2024 — McMap. All rights reserved.