【问题标题】:WPF: ContentTemplate.FindName returns "This operation is valid only on elements that have this template applied" - even after the template has loadedWPF:ContentTemplate.FindName 返回“此操作仅在应用了此模板的元素上有效” - 即使在模板已加载之后
【发布时间】:2018-05-29 11:05:08
【问题描述】:

我正在尝试创建一个 MarkupExtension,它将:

  1. 找到目标对象所在的DataTemplate/ContentTemplate。
  2. 在同一模板中查找另一个对象。
  3. 将目标对象的属性绑定到在该模板中找到的对象的属性。

这样做的原因是我希望能够使用ElementName 来绑定DataTemplates 中的源,这通常是不可能的。

我编写了以下 MarkupExtension(注意:这是一个快速的第一个版本,我只是希望它能够正常工作,此时不太关心优雅或效率):

using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Markup;
using System.Windows.Threading;

namespace Speedocs.WPF.MarkupExtensions
{
    public sealed class DataTemplateElementBinding : MarkupExtension
    {
        #region fields

        private FrameworkElement _targetObject;
        private DependencyProperty _targetProperty;
        private ContentPresenter _templatedParent;

        #endregion

        #region properties

        public string ElementName { get; set; }

        public string Path { get; set; }

        #endregion

        #region Overrides of MarkupExtension

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            var target = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
            if (target != null)
            {
                if (target.TargetObject.GetType().Name == "SharedDp") return this;

                _targetObject = target.TargetObject as FrameworkElement;
                if (_targetObject == null)
                {
                    return null;
                }

                _targetProperty = target.TargetProperty as DependencyProperty;
                if (_targetObject == null)
                {
                    return null;
                }

                // now that the target object has been loaded, find the requsted element
                // in the DataTemplate that contains this object, and bind the requested property
                // to that element
                _targetObject.Dispatcher.BeginInvoke(DispatcherPriority.Loaded, (Action) (() =>
                {
                    _templatedParent = _targetObject.TemplatedParent as ContentPresenter;
                    if (_templatedParent == null) return;

                    var sourceObject =
                        _templatedParent.ContentTemplate.FindName(ElementName, _templatedParent);
                    var binding = new Binding(Path) {Source = sourceObject};
                    _targetObject.SetBinding(_targetProperty, binding);
                }));
            }

            return null;
        }

        #endregion
    }
}

这个 MarkupExtension 的作用是:

  1. 如果从 IProvideValueTarget 返回的 TargetObject 是 SharedDp,则返回 MarkupExtension 本身,以便在目标值加载时再次调用它。
  2. 加载模板时,会再次调用ProvideValue。
  3. 然后我们从目标对象中获取 TemplatedParent,使用 FindName 在模板中找到源对象,并绑定到它。

问题是当我调用_templatedParent.ContentTemplate.FindName(ElementName, _templatedParent); 时出现错误

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

现在,我知道这个错误,它出现在这里没有意义,因为此时模板必须已经加载...如果没有,ProvideValue 就不会被调用第二次时间。

如您所见,我也尝试使用DispatcherPriority.Loaded 调用Dispatcher.BeginInvoke,但没有成功。

请帮忙:-\

【问题讨论】:

    标签: wpf


    【解决方案1】:

    您只需BeginInvoke 优先级为Loaded 的操作,但这与Framework.Loaded 事件不同。 MarkupExtension 将在分配给属性时立即评估其值,不仅是通常的 Controls,还有 Template

    如果您的代码在Template 中运行,您需要返回this,以便您的实际代码可以在实际控件中运行。只需在下面添加一个条件:

    if (!(serviceProvider.GetService(typeof(IProvideValueTarget)) is IProvideValueTarget service))
    {
        return null;
    }
    // Return this to provide lazy value when it is running in a template.
    if (service.TargetObject.GetType().Name.EndsWith("SharedDp"))
    {
        return this;
    }
    

    其实TargetObject类型的全称是System.Windows.SharedDp。但它是一个内部类型,所以它可能会被移动到另一个命名空间。我建议不要使用全名。

    在您的BeginInvoke 之前添加此条件,它会对您有所帮助。另外,你会发现如果你加上上面的条件,你可以安全地删除BeginInvoke


    这是我的运行结果:

    这是我的 XAML 测试代码(我粘贴了您的 MarkupExtension 并没有更改任何内容):

    <ListView>
        <ListView.ItemTemplate>
            <DataTemplate DataType="system:String">
                <Grid Name="rootGrid">
                    <TextBlock Text="{local:DataTemplateElementBinding ElementName=rootGrid, Path=Background}" />
                </Grid>
            </DataTemplate>
        </ListView.ItemTemplate>
        xyz
    </ListView>
    

    【讨论】:

    • 谢谢@walterlv。如您所见,我实际上在原始代码中确实存在这种情况。无论如何,由于某种未知的原因,它现在可以工作了,没有任何我记得的变化……至少没有直接的变化。哦,好吧。
    • @user884248 这很有趣!那么你能写点什么来帮助其他人在未来阅读你的问题吗?
    猜你喜欢
    • 1970-01-01
    • 2013-09-15
    • 1970-01-01
    • 2011-08-10
    • 1970-01-01
    • 2021-07-01
    • 2019-01-26
    • 1970-01-01
    • 2017-11-06
    相关资源
    最近更新 更多