Reputation: 1358
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 BindingExpression
from a Binding.Source
object, that is not associated with a DependencyProperty
.
Thanks in advance for your advice and assistance.
Upvotes: 3
Views: 2718
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:
var x = dobj.GetValue(dp); dobj.SetValue(dp, 12345); dobj.CoerceValue(dp); // etc.
BindingExpression
:b.Converter = new FooConverter(/* customized values here */); b.ConverterParameter = Math.PI; b.StringFormat = "---{0}---"; // ...as shown above
if (binding_not_needed) return null;
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 [see edit].ProvideValue
won't be called back again when the real TargetObject
is available
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