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!
CodePudding user response:
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));
}