为什么在自己的模板上,'this.ContentTemplate.FindName'会抛出InvalidOperationException异常?

22

好的... 这个问题让我困惑了。在我的UserControl子类中,我重写了OnContentTemplateChanged方法。我检查传入的newContentTemplate的值是否确实等于this.ContentTemplate(是的),但是当我调用这个...

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

...它抛出了以下异常...

"此操作仅适用于已应用此模板的元素。"

根据另一个相关问题中的评论者所说,您应该传递控件的内容呈现器而不是控件本身,因此我尝试了下面的代码...

var cp = FindVisualChild<ContentPresenter>(this);

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

...其中FindVisualChild只是MSDN示例中使用的帮助程序函数,用于查找相关联的内容呈现器。虽然找到了cp,但它也会抛出相同的错误。我困惑了!

以下是该帮助函数的参考:

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;
}
3个回答

20

在调用FindName方法之前明确应用模板将防止此错误发生。

this.ApplyTemplate(); 

1
天啊!如果真的那么简单(文档确实如此建议!),我会自责的!我想这澄清了模板已经被更改但是在这一点上还没有应用。你的调用确保它将被应用。太好了!! - Mark A. Donohoe
我这里有一个问题。 我正在ApplyTemplate()内部进行一些更改,只是为了覆盖某些控件的值。那么解决方案是什么? - Arijit
1
在我的情况下,将方法调用应用于ContentPresenter(调用FindName()的第二个参数)就可以了,即cp.ApplyTemplate()。 - bgh

4
正如John所指出的那样,在OnContentTemplateChanged被触发之前,它实际上还没有被应用到底层的ContentPresenter上。因此,您需要延迟调用FindName直到它被应用。可以尝试以下方式:
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);
}

另外,您可以将处理程序附加到UserControl的LayoutUpdated事件,但这可能比您想要的更频繁地触发。不过,这也会处理隐式DataTemplates的情况。

像这样:

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";
}

我仍然不同意微软将该函数命名为似乎尚未应用的名称,因此在我看来它实际上并没有改变(更不用说几乎相同命名的OnApplyTemplate函数可以避免这种情况),但是由于你给了我一个能够得到我想要结果的代码示例,所以我会接受。 - Mark A. Donohoe
你总是擅长这种事情。但是,即使是你的答案比下面@Adabyron的答案更复杂,他只是明确调用this.ApplyTemplate()吗?我必须找到那个旧代码进行测试,但根据文档,这正是它存在的原因,甚至告诉您如果VisualTree由于调用而更改。 - Mark A. Donohoe
@MarqueIV - 是的,那似乎是一个更简单的解决方案,对吧?现在我这个的唯一好处是它可以适应隐式应用的DataTemplates(即通过隐式样式等)。 - CodeNaked

0

ContentPresenter在事件之后才应用ContentTemplate。虽然此时ContentTemplate属性已在控件上设置,但它尚未向ControlTemplate内部的绑定(如ContentPresenter的ContentTemplate)推送。

您最终想要使用ContentTemplate做什么?可能有更好的整体方法来达到您的最终目标。


那么为什么还要有这个事件呢?它确实说的是“Changed”,而不是“Preview”或“WillChange”。根据你所说,它实际上还没有改变。而且错误信息肯定是误导人的。(另外,我只是按照别人的建议尝试了ContentPresenter的东西。我正在使用一个UserControl,所以我没有使用ContentPresenter。我只是通过资源中的模板替换用户控件中现有的标记。至于我想做什么,就是获取由该模板交换的元素之一,我认为上面的代码应该已经显示了。) - Mark A. Donohoe
我想到的实现方法是定义一个类级别的变量来保存控件引用。然后通过模板将Loaded事件附加到它上面。然后在代码后台,我只需将“sender”存储在变量中。然后使用OnContentTemplateChanged调用将该变量设置为“null”。这是一种hacky的方法,但它确实有效。不过,为什么会有上述事件,你可以直接使用模板却不能做任何事情呢?这毫无意义,特别是因为OnApplyTemplate 确实应用了它。按照这个想法,这也应该这样做。 - Mark A. Donohoe
你应该在运行时使用像Snoop这样的工具更仔细地查看你的可视树,以更好地理解实际呈现的内容。UserControl是一个ContentControl,这意味着它在其默认模板中使用ContentPresenter来显示内容。ContentTemplate通过绑定应用于已分配给UserControl的ContentTemplate属性之后,被应用于该ContentPresenter。很明显你正在尝试获取EditTextBox,但为什么?通常情况下,您可以摆脱事件代码并改用数据绑定。 - John Bowen
“ContentTemplate被分配给UserControl的ContentTemplate属性后,通过绑定应用于该ContentPresenter。那么我该如何检测到这种变化呢?根据您自己的定义(和控件),ContentTemplate已经设置为控件,因此该绑定应该已经被执行了,那么为什么FindName不起作用呢?在我执行base.OnContentTemplateChanged之前,为什么该绑定还没有被更新呢?换句话说,当内容模板改变可视树时,我怎样才能立即获取文本框?” - Mark A. Donohoe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接