如何从一个`Binding`对象中获取`BindingExpression`?

3
简而言之,我有一个 ListView(目标),它单向绑定到一个 XmlDataProvider(源),这个 XmlDataProvider(源)是双向绑定到一个TextBox(目标)的,使用标准 XAML 控件绑定和自定义 XAML 扩展来绑定到 XmlDataProvider。这对于应用程序来说很方便,因为在应用程序运行后,XmlDataProvider 是从用户输入动态加载的。
无论如何,在运行时,在修改了 TextBox.Text 属性后,将调用 IMultiValueConverter.ConvertBack(...) 方法,以将更新从此目标传播回到源。但是,由于 XmlDataProvider 对象不是 DependencyProperty,因此更新不会从已更改的 XmlDataProvider 源进一步传播到其他绑定到 ListView 目标的位置。
如果没有重新设计(您可以合理地建议),我需要通知 WPF,任何具有此 XmlDataProvider 作为源的目标都需要进行更新。我希望保持一个通用的、可重用的绑定类,并且迄今为止,我已经享受了我的大部分 XAML 解决方案的低编码负担。
当前,我唯一的代码后续访问是从 IMultiValueConverter.ConvertBack(...) 方法中。在此方法中,我可以访问 XmlDataProvider<-->TextBox 链接的Binding 对象。如果我能够获取 Binding.Source 对象的 BindingExpression,那么我就可以调用 BindingExpression.UpdateTarget() 来完成更新传播。
但是,我不知道如何从没有与 DependencyProperty 关联的 Binding.Source 对象获取 BindingExpression
提前感谢您的建议和帮助。

你唯一能够做到这一点的方法是使用Binding.ProvideValue方法(实际上返回一个BindingExpression),但该方法需要一些IServiceProvider作为参数。我所见过的唯一可以访问此接口的地方是在某些自定义MarkupExtensionProvideValue实现内部。因此看起来真的卡在那里了。 - King King
这是我之前不知道的一个途径,值得一看。 - ergohack
1个回答

5
您可以创建一个自定义的MarkupExtension,它接受一个Binding作为构造函数参数。在XAML中使用时,您的绑定将是包装WPF绑定的外部绑定:
<StackPanel>
    <TextBox x:Name="tb" />
    <TextBlock Text="{local:MyBinding {Binding ElementName=tb,Path=Text,Mode=OneWay}}" />
</StackPanel>

MyBinding构造函数中,您将收到一个WPF Binding对象。 请存储一份副本以备稍后调用ProvideValue时使用。 在那时,您可以在保存的绑定上调用ProvideValue - 并将其传递给您现在拥有的IServiceProvider实例。 您将获得一个BindingExpression,然后可以从您自己的 ProvideValue中返回它。
这是一个最简示例。 对于简单演示,它只是添加(或覆盖)内部(包装)绑定的Binding.StringFormat属性。
[MarkupExtensionReturnType(typeof(BindingExpression))]
public sealed class MyBindingExtension : MarkupExtension
{
    public MyBindingExtension(Binding b) { this.m_b = b; }

    Binding m_b;

    public override Object ProvideValue(IServiceProvider sp)
    {
        m_b.StringFormat = "---{0}---";   // modify wrapped Binding first...

        return m_b.ProvideValue(sp);    // ...then obtain its BindingExpression
    }
}

如果您使用上面的XAML进行尝试,您将看到目标确实设置了实时绑定,并且您根本不需要解包IProvideValueTarget
这涵盖了基本的见解,因此如果您现在确切地知道该怎么做,您可能不需要阅读本答案的其余部分...

更多细节

在大多数情况下,深入了解 IProvideValueTarget 实际上是整个练习的重点,因为您可以根据运行时条件动态地 修改包装的绑定。下面展开的 MarkupExtension 显示了相关对象和属性的提取,显然有许多可能性。

[MarkupExtensionReturnType(typeof(BindingExpression))]
[ContentProperty(nameof(SourceBinding))]
public sealed class MyBindingExtension : MarkupExtension
{
    public MyBindingExtension() { }
    public MyBindingExtension(Binding b) => this.b = b;

    Binding b;
    public Binding SourceBinding
    {
        get => b;
        set => b = value;
    }

    public override Object ProvideValue(IServiceProvider sp)
    {
        if (b == null)
            throw new ArgumentNullException(nameof(SourceBinding));

        if (!(sp is IProvideValueTarget pvt))
            return null;                // prevents XAML Designer crashes

        if (!(pvt.TargetObject is DependencyObject))
            return pvt.TargetObject;    // required for template re-binding

        var dp = (DependencyProperty)pvt.TargetProperty;

        /*** INSERT YOUR CODE HERE ***/

        // finalize binding as a BindingExpression attached to target
        return b.ProvideValue(sp);
    }
};

为了完整性,这个版本也可以与XAML对象标签语法一起使用,其中包装的Binding被设置为属性,而不是在构造函数中设置。
在指定位置插入自定义代码以操作绑定。您可以在这里做任何想做的事情,例如:
1.检查或修改运行时情况和/或状态: ``` var x = dobj.GetValue(dp); dobj.SetValue(dp, 12345); dobj.CoerceValue(dp); // etc. ```
2.在将绑定封闭到BindingExpression之前重新配置/自定义绑定: ``` b.Converter = new FooConverter(/* customized values here */); b.ConverterParameter = Math.PI; b.StringFormat = "---{0}---"; // ...as shown above ```
3.也许决定在某些情况下不需要绑定;不要继续进行绑定: ``` if (binding_not_needed) return null; ```
  1. 还有很多,只受你的想象力限制。准备好后,调用绑定的 ProvideValue 方法,它会创建自己的 BindingExpression。因为你传递了自己的 IProvideValueTarget 信息(即你的 IServiceProvider),新的绑定将替换掉你的标记扩展。它会附加到目标对象/属性上,在那里你的 标记扩展 在 XAML 中被编写,这正是你想要的。

奖励:你还可以操作返回的 BindingExpression

如果预配置绑定不足够,注意你也可以访问已实例化的 BindingExpression。不要像所示地 tail-calling 返回 ProvideValue 结果,而是将结果存储在本地。在返回之前,你可以通过 BindingExpression 上可用的各种通知选项设置监视或拦截绑定流量。



最后说明:如此处所讨论,当WPF标记扩展用于模板内部时,有特殊考虑事项。特别是,您会注意到,您的标记扩展最初被探测到,并将IProvideValueTarget.TargetObject设置为System.Windows.SharedDp的实例。因为加载模板是一个延迟的过程,我认为这里的目的是对您的标记扩展进行早期探测,以确定其特性,即在任何真实数据存在之前,可以正确填充模板。如上面的代码所示,您 [必须返回'this'] 可以返回探测对象本身来处理这些情况;如果不这样做,当真正的TargetObject可用时,您的ProvideValue将不会再次被调用 [参见编辑]。


编辑: WPF非常努力地使XAML资源可共享,特别是包括BindingBase和派生类。如果在可重用的上下文(例如Template)中使用我在这里描述的技术,则需要确保包装的绑定不符合可共享的标准,否则在生成BindingExpression后,包装的绑定将变为BindingBase.isSealed=true;尝试修改Binding将失败,并显示以下内容:

InvalidOperationException:在使用后无法更改绑定。

有几种解决方法可以做到这一点,您可以通过研究(非公开)WPF函数TemplateContent.TrySharingValue的源代码来确定。我发现的一种方法是从您的标记扩展中返回System.Windows.SharedDp对象,每次它出现时都是如此。您可以通过查找任何非DependencyObject值或更具体地按以下方式检测System.Windows.SharedDp

if (pvt.TargetObject.GetType().Name == "SharedDp")
   return pvt.TargetObject;

(从技术角度来看,检查 .GUID 值是否为 {00b36157-dfb7-3372-8b08-ab9d74adc2fd} 才是最正确的方法)。我已经更新了原帖中的代码以反映这一点,但我欢迎进一步的见解,以便在模板与非模板两种用例中实现最大资源共享。


编辑:我认为,为了在模板使用中进行可共享性确定,返回this(如我最初建议的那样)和我的修订建议返回pvt.TargetObject之间的主要区别在于前者源自MarkupExtension--而System.Windows.SharedDp的基类是Object--并且很明显探测代码会递归到嵌套的标记扩展中。


我在找到这个答案之前就自己尝试过了,它确实可以在运行时工作。然而,在设计器中我得到了错误XLS0504,属性“Text”不支持“MyBinding(BindingExpression)”类型的值。你有遇到过这个问题吗? - Michael Wagner

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