首页 > 解决方案 > WPF:ContentTemplate.FindName 返回“此操作仅对应用了此模板的元素有效” - 即使在模板已加载之后

问题描述

我正在尝试创建一个 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 就不会被第二次调用。

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

请帮忙 :-\

标签: wpf

解决方案


你只是BeginInvoke把动作放在一个Loaded优先级上,但这与Framework.Loaded事件不一样。AMarkupExtension将在分配给属性时立即评估其值,不仅是通常Control的 s,还有Template.

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

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

实际上,该类型的全称TargetObjectSystem.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>

推荐阅读