ergohack
ergohack

Reputation: 1358

How do I get a `BindingExpression` from a `Binding` object?

In short(?), I have a ListView (target) one-way bound to an XmlDataProvider (source) two-way bound to a TextBox (target) using standard XAML for the control bindings and custom XAML extensions for the bindings to the XmlDataProvider. This is a convenience for the application as the XmlDataProvider is dynamically loaded from user inputs after the application is running,...

Anyway, at run-time, after modifying the TextBox.Text property, the IMultiValueConverter.ConvertBack(...) method is called to propagate the update from this target back to the source. But, because the XmlDataProvider object is not a DependencyProperty, the update is not further propagated from the changed XmlDataProvider source to the other binding to the ListView target.

Without rearchitecting, which you could legitimately advise, I need to notify WPF that any target with this XmlDataProvider as a source needs to be updated. I am hoping to maintain a generic, reusable binding class and have, so far, enjoyed the low coding burden of my mostly XAML solution.

Currently, the only code behind access I have is from within the IMultiValueConverter.ConvertBack(...) method. From within this method I do have access to the Binding object for the XmlDataProvider <--> TextBox link. If I could get the BindingExpression for the Binding.Source object I could then make the call to BindingExpression.UpdateTarget() to complete the update propagation,...

But, I do not know how to get a BindingExpressionfrom a Binding.Source object, that is not associated with a DependencyProperty.

Thanks in advance for your advice and assistance.

Upvotes: 3

Views: 2718

Answers (1)

Glenn Slayden
Glenn Slayden

Reputation: 18749

You can create a custom MarkupExtension which accepts a Binding as a constructor argument. In XAML usage, yours will be an outer binding that wraps the WPF one:

<StackPanel>
    <TextBox x:Name="tb" />
    <TextBlock Text="{local:MyBinding {Binding ElementName=tb,Path=Text,Mode=OneWay}}" />
</StackPanel>

In the MyBinding constructor you will receive a WPF Binding object. Store a copy for later when your ProvideValue is called. At that time, you can call ProvideValue on the binding you saved--and pass it the IServiceProvider instance you now have. You'll get back a BindingExpression that you can then return from your own ProvideValue.

Here's a minimal example. For a simple demonstration, it just adds (or overwrites) a Binding.StringFormat property to the inner (wrapped) binding.

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

If you try it with the XAML above, you'll see that a live binding is indeed set on the target, and you didn't have to unpack the IProvideValueTarget at all.

This covers the basic insight, so if you know exactly what to do now, you probably won't need to read the rest of this answer...


More details

In most cases, digging into the IProvideValueTarget is actually the point of the whole exercise, because you can then modify the wrapped binding dynamically according to runtime conditions. The expanded MarkupExtension below shows the extraction of the relevant objects and properties, and there are obviously numerous possibilities for what you can do from there.

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

For completeness, this version can also be used with XAML object tag syntax, where the wrapped Binding is set as a property, instead of in the constructor.

Insert your customization code for manipulating the binding where indicated. You can do pretty much anything you want here, such as:

  1. Check or modify the runtime situation and/or state:
var x = dobj.GetValue(dp);
dobj.SetValue(dp, 12345);
dobj.CoerceValue(dp); // etc.
  1. Reconfigure and/or customize the binding prior to sealing it into the BindingExpression:
b.Converter = new FooConverter(/* customized values here */);
b.ConverterParameter = Math.PI;
b.StringFormat = "---{0}---";   // ...as shown above
  1. Perhaps decide binding is not needed in certain cases; do not proceed with binding:
if (binding_not_needed)
    return null;
  1. Lots more, limited by your imagination. When ready, call the binding's ProvideValue method and it will create its BindingExpression. Because you pass it your own IProvideValueTarget info (i.e. your IServiceProvider), the new binding will substitute itself for your markup extension. It gets attached to the target object/property where your MarkupExtension was authored in XAML, which is exactly what you want.

Bonus: You can also manipulate the returned BindingExpression

If pre-configuring the binding isn't enough, note that you also have access to the instantiated BindingExpression. Instead of tail-calling the ProvideValue result as shown, just store the result into a local. Prior to returning it, you can set up monitoring or interception of the binding traffic via the various notification options that are available on BindingExpression.


Final note: as discussed here, there are special considerations when WPF markup extensions are used inside templates. In particular, you will notice that your markup extension is initially probed with IProvideValueTarget.TargetObject set to an instance of System.Windows.SharedDp. Because loading templates is a naturally a deferred process, I believe the purpose here is early probing of your markup extension to determine its characteristics, i.e. long prior to the existence of any real data which could populating the template properly. As shown in the above code, you [must return 'this'] c̲a̲n̲ r̲e̲t̲u̲r̲n̲ t̲h̲e̲ p̲r̲o̲b̲e̲ o̲b̲j̲e̲c̲t̲ i̲t̲s̲e̲l̲f̲ for these cases; if you don't, your ProvideValue won't be called back again when the real TargetObject is available [see edit].


edit: WPF tries really hard to make XAML resources shareable, and this especially includes the BindingBase and derived classes. If using the technique I describe here in a reusable context (such as a Template), you need to make sure that the wrapped binding does not meet the criteria for shareability, otherwise the wrapped binding will become BindingBase.isSealed=true after the first time it generates a BindingExpression; subsequent attempts to modify the Binding will fail with:

InvalidOperationException: Binding cannot be changed after it has been used.

There are several workarounds to do this, which you can ascertain by studying the source code of the (non-public) WPF function TemplateContent.TrySharingValue. One method I found was to return the System.Windows.SharedDp object from your markup extension anytime it shows up. You can detect System.Windows.SharedDp either by looking for any non-DependencyObject value, or more specifically as follows:

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

(Technically, checking for .GUID value {00b36157-dfb7-3372-8b08-ab9d74adc2fd} instead would the most correct). I've updated the code in my original post to reflect this, but I welcome further insight on how to preserve maximal resource sharing for both of the use cases, template vs. non-template.


edit: I'm thinking that, for the purposes of the sharability determination in template usage, the main difference between returning this (as I had originally suggested) and my revised suggestion to return pvt.TargetObject is that the former derives from MarkupExtension--versus the base class of System.Windows.SharedDp being Object--and it's clear that the probing code recurses into nested markup extensions.

Upvotes: 5

Related Questions