How do I resolve the value of a databinding inside a MarkupExtension?
Asked Answered
A

2

7

I've made a markup extension for translating strings based on a key. Example

<TextBlock Text="{Translate myKey}" />

Now I want to be able to use nested bindings for providing my keys. Example:

<TextBlock Text="{Translate {Binding KeyFromDataContext}}" />

When I do this I get a System.Windows.Data.Binding object. By calling ProvideValue and passing down the ServiceProvider I can get a BindingExpression:

var binding = Key as Binding;
if (binding == null) {
    return null;
}
var bindingExpression = binding.ProvideValue(_serviceProvider) as BindingExpression;
if (bindingExpression == null) {
    return null;
}
var bindingKey = bindingExpression.DataItem;

I can get this bindingExpression, but the DataItem property is null. I've tested my binding like this

<TextBlock Text="{Binding KeyFromDataContext}" />

and it works fine.

Any ideas?

Afc answered 12/6, 2009 at 9:50 Comment(1)
Does this answer your question? MarkupExtension that uses a DataBinding valueSocrates
B
2

The toxvaerd's answer is not universal. It breaks if the original binding already had a converter. Or when writing a converter is not possible.

There's a better solution. We can declare two constructors. The second one accepting BindingBase will be called by XAML when a binding is used. To resolve the value of the binding, we can declare a private attached property. For this to work we need to know the target element of the markup extension.

There's a catch: when the markup extension is used inside a template, there is no target element (obviously). In this case you are supposed to use return this in ProvideValue() - this way the extension will be called again when the template is applied.

public class TranslateExtension : MarkupExtension
{
    private readonly BindingBase _binding;

    public TranslateExtension(BindingBase binding)
    {
        _binding = binding;
    }

    public TranslateExtension(string key)
    {
        Key = key;
    }

    [ConstructorArgument("key")]
    public string Key { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        if (_binding != null)
        {
            var pvt = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
            var target = pvt.TargetObject as DependencyObject;

            // if we are inside a template, WPF will call us again when it is applied
            if (target == null)
                return this; 

            BindingOperations.SetBinding(target, ValueProperty, _binding);
            Key = (string)target.GetValue(ValueProperty);
            BindingOperations.ClearBinding(target, ValueProperty);
        }

        // now do the translation using Key
        return ...;
    }

    private static readonly DependencyProperty ValueProperty = 
        DependencyProperty.RegisterAttached("Value", typeof(string), typeof(TranslateExtension));
}
Braga answered 4/1, 2018 at 6:45 Comment(6)
What if the binding value changes, ProvideValue doesn't get called again right? Is there any solution for this?Emigrant
@Haitam the question was about resolving the value of the binding. If you suspect it might change - you can instead return another binding that binds to Value and uses a converter.Braga
Can you, please, explain how the attached property here works? I am always getting null from target.GetValue() ...Dicephalous
@Dicephalous your binding itself is probably wrong. Post a minimal example as a separate question please.Braga
torvin, my original binding works fine, I got it to work with ygoe's solution. I don't have time to post a new question at the moment, might do it when I revisit this.Dicephalous
While this may somehow have worked by accident, it is not at all a general solution. For reference: https://mcmap.net/q/83401/-markupextension-that-uses-a-non-string-databinding-value/1136211Leslielesly
A
2

It is not possible to get the value of a binding. You're not supposed to be even trying to do this. WPF uses some fancy reflection to resolve the bindings and trust me - you do not wan't to start trying that yourself.

Anyway with that in mind, this is what I ended up doing, which actually is a nice solution:

I made a TranslateConverter that took care of the translation:

public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
    var key = value as string ?? parameter as string;

    if (key != null)
    {
    // Do translation based on the key

    }
    return null;
}

Then in my TranslateExtension I simply do this:

var binding = Key as Binding ?? new Binding{Mode = BindingMode.OneWay};
binding.Converter = new TranslateConverter(_targetObject, _targetProperty, Dictionary, Converter);
binding.ConverterParameter = Key is Binding ? null : Key as string;

return binding.ProvideValue(serviceProvider);

This way a binding is resolved by WPF and is passed to the converter as value, while a simple text-key is passed to the converter as a paramter.

_targetObject and _targetProperty are obtained from the ServiceProvider.

Afc answered 17/6, 2009 at 11:24 Comment(1)
What does that mean? A data binding's one and only use it to transport a value and keep it up-to-date. How can a binding not have a value then? I'm looking for the same solution but you didn't answer your question, you answered something else. A converter is not an option for me.Hedonism
B
2

The toxvaerd's answer is not universal. It breaks if the original binding already had a converter. Or when writing a converter is not possible.

There's a better solution. We can declare two constructors. The second one accepting BindingBase will be called by XAML when a binding is used. To resolve the value of the binding, we can declare a private attached property. For this to work we need to know the target element of the markup extension.

There's a catch: when the markup extension is used inside a template, there is no target element (obviously). In this case you are supposed to use return this in ProvideValue() - this way the extension will be called again when the template is applied.

public class TranslateExtension : MarkupExtension
{
    private readonly BindingBase _binding;

    public TranslateExtension(BindingBase binding)
    {
        _binding = binding;
    }

    public TranslateExtension(string key)
    {
        Key = key;
    }

    [ConstructorArgument("key")]
    public string Key { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        if (_binding != null)
        {
            var pvt = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
            var target = pvt.TargetObject as DependencyObject;

            // if we are inside a template, WPF will call us again when it is applied
            if (target == null)
                return this; 

            BindingOperations.SetBinding(target, ValueProperty, _binding);
            Key = (string)target.GetValue(ValueProperty);
            BindingOperations.ClearBinding(target, ValueProperty);
        }

        // now do the translation using Key
        return ...;
    }

    private static readonly DependencyProperty ValueProperty = 
        DependencyProperty.RegisterAttached("Value", typeof(string), typeof(TranslateExtension));
}
Braga answered 4/1, 2018 at 6:45 Comment(6)
What if the binding value changes, ProvideValue doesn't get called again right? Is there any solution for this?Emigrant
@Haitam the question was about resolving the value of the binding. If you suspect it might change - you can instead return another binding that binds to Value and uses a converter.Braga
Can you, please, explain how the attached property here works? I am always getting null from target.GetValue() ...Dicephalous
@Dicephalous your binding itself is probably wrong. Post a minimal example as a separate question please.Braga
torvin, my original binding works fine, I got it to work with ygoe's solution. I don't have time to post a new question at the moment, might do it when I revisit this.Dicephalous
While this may somehow have worked by accident, it is not at all a general solution. For reference: https://mcmap.net/q/83401/-markupextension-that-uses-a-non-string-databinding-value/1136211Leslielesly

© 2022 - 2024 — McMap. All rights reserved.