MarkupExtension that uses a non-string DataBinding value
Asked Answered
P

1

0

I'm trying to create a MarkupExtension for WPF for use with translation. I have found some similar questions asked here including

MarkupExtension that uses a DataBinding value

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

ultimately, this lead to the response by Torvin that looks really promising. However, just as a person in the comments, I have an issue where the value obtained by the target.GetValue() is always returning null.

Here's some of the code.

Ultimately I have a set of static classes that contains a static KeyDefinition object that looks like the following

Public class KeyDefinition
{
   Public string Key {get; set;}
   Public string DefaultValue {get; set;}
}

The key ties back to a JSON resource while the DefaultValue is an English translation that we can use for Design Time display of the xaml.

Localization occurs through a static class like so Localize.GetResource(key)

My goal is to write XAML like this

<TextBlock Text="{Localize {Binding KeyDefinitionFromDataContext}}">

where KeyDefinitionFromDataContext is a property in the view model that returns a reference to a KeyDefinition object.

As per Torvin's response I created a MarkupExtension like so

public class LocalizeExtension : MarkupExtension
{
  private readonly BindingBase _binding;
  private static readonly DependencyProperty _valueProperty = DependencyProperty.RegisterAttached("Value", typeof(KeyDefinition), typeof(LocalizeExtension));
  
  [ConstructorArgument("keyDefinition")
  public KeyDefinition KeyDefinition {get; set;}

  public LocalizeExtension(Binding binding)
  {
    _binding = binding;
  }

  public LocalizeExtension(KeyDefinition keyDefinition)
  {
    KeyDefinition = keyDefinition;
  }

  public override object ProvideValue(IServiceProvider serviceProvider)
  {
    var pvt = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
    var target = pvt.TargetObject as DependencyObject;
    var property = pvt.TargetProperty as DependencyProperty;

    //If inside a template, WPF will call again when its applied
    if (target == null)
      return this;

    BindingOperations.SetBinding(target, property, _binding);
    KeyDefinition = (KeyDefinition)target.GetValue(_valueProperty);
    BindingOperations.ClearBinding(target, property);

    return Localize.GetResource(KeyDefinition.Key);
  }
}

Now please forgive me, because I do not usually do WPF work, but this task has fallen to me. Whenever I run this code the value returned is always Null. I've tried using strings directly instead of the 'KeyDefinition' object but run into the same problem.

I think what confuses me here is how the DependencyProperty on the target ever gets set because its private.

Any help is appreciated. Thanks!

Pilgrimage answered 23/10, 2021 at 3:30 Comment(5)
Your LocalizeExtension class won't even compile, at least not when KeyDefinition is a static class. A static class can not be used as the type of a property or a constructor argument.Cu
You would also have to use the _valueProperty field in the SetBinding, GetValue, and ClearBinding calls, not the local property variable (which is then useless). But even with that modification, the approach won't work. It looks like something that worked by accident, and that you should not be trying to do. Go with the other approach and attach a Binding Converter.Cu
Is the end aim localisation? I wouod usually do that by using a resourcedictionary per language with all the strings in it. Merge in whichever language is required and use dynamicresource with the key in the view. Merge in french and it's keys match the otiginal english keys used in the views and you see french.Untuck
Sorry. There was a typo when I wrote the question. The KeyDefinition class is not supposed to be static. That has been updated. I agree with you Clemens I think. I don’t really see how this will work. I’m going to give the other approach a try.Pilgrimage
Ideally I’d just respond to the question in the other threat and ask for clarification but I don’t have enough rep to do so.Pilgrimage
T
1

This is not how it works. The result of the MarkupExtension is always null, because that's what you return. You must know that the Binding (the BindingExpression) is not resolved at the time the extension is invoked. The XAML engine invokes the extension and expects a expression in case of a Binding. Normally, the MarkupExtension would return the result of Binding.ProvideValue(serviceProvider), which is a BindingExpressionBase. The XAML engine will later use this expressions to generate data by actually attaching the binding.

In other words, you return the result prematurely.

Aside from that, you must also know that the MarkupExtension.ProvideValue is only called once. This means your extension does not handle property changes (in case the binding source changes) and clearing the binding is not the desired handling of the binding. It actually even fails to handle a OneTime binding mode.
In the context of localization, it makes pretty much sense to expect the source property to change, at least when the user changes the localization.

There more errors in your code, like an unset _valueProperty field. And what is the purpose of defining a DependencyProperty on a type that does not extend DependencyObject? It's even private! You should also avoid mixing properties and fields. Better define (read-only) properties instead of fields. Returning this (the instance of type MarkupExtension) from your extension will not work where the expected type is other than object e.g., a string - return null instead.

What you want is very simple to realize.
First, you must attach the Binding to a proxy object in order to allow the binding engine to activate the BindingExpression (in the example this is the BindingResolver class).
Second, you must configure the incoming binding to raise notification when the target is updated. Then listen to the Binding.TargetUpdated event to realizes OneWay binding. To realize TwoWay and OneWayToSource binding modes, you must also enable and observe the Binding.SourceUpdated event.
And finally, retrieve the changed value from the source/binding proxy to set it to the target of the MarkupExtension.

Since data binding usually involve having the DataContext as source i.e. requires the visual tree in order to resolve, the binding proxy is a simple attached property. This has the advantage that we can use the original DataContext of the target element and don't have to worry about how to inject our proxy into the visual tree.

LocalizeExtension.cs

public class LocalizeExtension : MarkupExtension
{
  private Binding Binding { get; };
  private DependencyObject LocalizationTarget { get; set; }
  private DependencyProperty LocalizationTargetProperty { get; set; }
  private object LocalizationSource { get; set; }
  private string LocalizationPropertyName { get; set; }
  private bool IsInitialized { get; set; }


  public LocalizeExtension(Binding binding)
  {
    this.Binding = binding;
    this.Binding.NotifyOnTargetUpdated = true;
  }

  public override object ProvideValue(IServiceProvider serviceProvider)
  {
    var serviceProvider = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
    this.LocalizationTarget = serviceProvider.TargetObject as DependencyObject;

    // If inside a template, WPF will call again when its applied
    if (this.LocalizationTarget == null)
    {
      return null;
    }

    this.LocalizationTargetProperty = serviceProvider.TargetProperty as DependencyProperty;

    BindingOperations.SetBinding(this.LocalizationTarget, BindingResolver.ResolvedBindingValueProperty, this.Binding);
    Binding.AddTargetUpdatedHandler(this.LocalizationTarget, OnBindingSourceUpdated);
    return null;
  }

  private void OnBindingSourceUpdated(object sender, EventArgs e)
  {
    if (!this.IsInitialized)
    {
      InitializeLocalizationSourceInfo();
    }

    LocalizeBindingSource();
  }

  private void InitializeLocalizationSourceInfo()
  {
    BindingExpression bindingExpression = BindingOperations.GetBindingExpression(this.LocalizationTarget, BindingResolver.ResolvedBindingValueProperty);
    this.LocalizationSource = bindingExpression.ResolvedSource;
    this.LocalizationPropertyName = bindingExpression.ResolvedSourcePropertyName;
    this.IsInitialized = true;
  }

  private void LocalizeBindingSource()
  {
    object unlocalizedValue = BindingResolver.GetResolvedBindingValue(this.LocalizationTarget);
    object localizedValue = LocalizeValue(unlocalizedValue);
    this.LocalizationTarget.SetValue(this.LocalizationTargetProperty, localizedValue);
  }

  private object LocalizeValue(object value)
  {
    return value is KeyDefinition keyDefinition 
      ? Localize.GetResource(keyDefinition.Key) 
      : string.Empty;
  }
}

BindingResolver.cs

class BindingResolver : DependencyObject
{
  public static object GetResolvedBindingValue(DependencyObject obj) => (object)obj.GetValue(ResolvedBindingValueProperty);
  public static void SetResolvedBindingValue(DependencyObject obj, object value) => obj.SetValue(ResolvedBindingValueProperty, value);

  public static readonly DependencyProperty ResolvedBindingValueProperty =
      DependencyProperty.RegisterAttached(
        "ResolvedBindingValue", 
        typeof(object), 
        typeof(BindingResolver), 
        new PropertyMetadata(default));
}
Theo answered 23/10, 2021 at 15:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.