How to Bind to a Dynamicresource So You Can Use a Converter or Stringformat, etc.? (Revision 4)

How can you bind to a DynamicResource so you can use a Converter or StringFormat, etc.? (Revision 4)

There's something I've always felt was a bit of missing functionality in WPF: the ability to use a dynamic resource as the source of a binding. I understand technically why this is--in order to detect changes, the source of a binding has to be a property on a DependencyObject or on an object that supports INotifyPropertyChanged, and a dynamic resource is actually a Microsoft-internal ResourceReferenceExpression which equates to the value of the resource (i.e. it's not an object with a property to bind to, let alone one with change notification)--but still, it's always bugged me that as something that can change during run-time, it should be able to be pushed through a converter as needed.

Well, I believe I've finally rectified this limitation...

Enter DynamicResourceBinding!

Note: I call it a 'Binding' but technically it's a MarkupExtension on which I've defined properties such as Converter, ConverterParameter, ConverterCulture, etc., but which does ultimately use a binding internally (several, actually!) As such, I have named it based on its usage, not its actual type.

But why?

So why would you even need to do this? How about globally scaling your font size based on a user preference while still being able to utilize relative font sizes thanks to a MultiplyByConverter? Or how about defining app-wide margins based simply on a double resource by using a DoubleToThicknessConverter that not only converts it to a thickness, but lets you mask out edges as needed in the layout? Or how about defining a base ThemeColor in a resource, then using a converter to lighten or darken it, or change its opacity depending on usage thanks to a ColorShadingConverter?

Even better, implement the above as MarkupExtensions and your XAML is simplified too!

<!-- Make the font size 85% of what it would normally be here -->
<TextBlock FontSize="{res:FontSize Scale=0.85)" />

<!-- Use the common margin, but suppress the top edge -->
<Border Margin="{res:Margin Mask=1011)" />

In short, this helps consolidate all the 'base values' in your main resources, but be able to tweak them when and where they're used without having to cram 'x' number of variations to them in your resources collection.

The Magic Sauce

The implementation of DynamicResourceBinding is thanks to a neat trick of the Freezable data type. Specifically...

If you add a Freezable to the Resources collection of a FrameworkElement, any dependency properties on that Freezable object which are set as dynamic resources will resolve those resources relative to that FrameworkElement's position in the Visual Tree.

Using that bit of 'magic sauce', the trick is to set a DynamicResource on a DependencyProperty of a proxy Freezable object, add that Freezable to the resource collection of the target FrameworkElement, then set up a binding between the two, which is now allowed, since the source is now a DependencyObject (i.e. a Freezable.)

The complexity is getting the target FrameworkElement when using this in a Style, as a MarkupExtension provides its value where it's defined, not where its result is ultimately applied. This means when you use a MarkupExtension directly on a FrameworkElement, its target is the FrameworkElement as you would expect. However, when you use a MarkupExtension in a style, the Style object is the target of the MarkupExtension, not the FrameworkElement where it's applied. Thanks to the use of a second, internal binding, I've managed to get around this limitation as well.

That said, here's the solution with comments inline:

DynamicResourceBinding

The 'Magic Sauce!' Read the inline comments for what's going on

public class DynamicResourceBindingExtension : MarkupExtension {

public DynamicResourceBindingExtension(){}
public DynamicResourceBindingExtension(object resourceKey)
=> ResourceKey = resourceKey ?? throw new ArgumentNullException(nameof(resourceKey));

public object ResourceKey { get; set; }
public IValueConverter Converter { get; set; }
public object ConverterParameter { get; set; }
public CultureInfo ConverterCulture { get; set; }
public string StringFormat { get; set; }
public object TargetNullValue { get; set; }

private BindingProxy bindingSource;
private BindingTrigger bindingTrigger;

public override object ProvideValue(IServiceProvider serviceProvider) {

// Get the binding source for all targets affected by this MarkupExtension
// whether set directly on an element or object, or when applied via a style
var dynamicResource = new DynamicResourceExtension(ResourceKey);
bindingSource = new BindingProxy(dynamicResource.ProvideValue(null)); // Pass 'null' here

// Set up the binding using the just-created source
// Note, we don't yet set the Converter, ConverterParameter, StringFormat
// or TargetNullValue (More on that below)
var dynamicResourceBinding = new Binding() {
Source = bindingSource,
Path = new PropertyPath(BindingProxy.ValueProperty),
Mode = BindingMode.OneWay
};

// Get the TargetInfo for this markup extension
var targetInfo = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));

// Check if this is a DependencyObject. If so, we can set up everything right here.
if(targetInfo.TargetObject is DependencyObject dependencyObject){

// Ok, since we're being applied directly on a DependencyObject, we can
// go ahead and set all those missing properties on the binding now.
dynamicResourceBinding.Converter = Converter;
dynamicResourceBinding.ConverterParameter = ConverterParameter;
dynamicResourceBinding.ConverterCulture = ConverterCulture;
dynamicResourceBinding.StringFormat = StringFormat;
dynamicResourceBinding.TargetNullValue = TargetNullValue;

// If the DependencyObject is a FrameworkElement, then we also add the
// bindingSource to its Resources collection to ensure proper resource lookup
if (dependencyObject is FrameworkElement targetFrameworkElement)
targetFrameworkElement.Resources.Add(bindingSource, bindingSource);

// And now we simply return the same value as if we were a true binding ourselves
return dynamicResourceBinding.ProvideValue(serviceProvider);
}

// Ok, we're not being set directly on a DependencyObject (most likely we're being set via a style)
// so we need to get the ultimate target of the binding.
// We do this by setting up a wrapper MultiBinding, where we add the above binding
// as well as a second binding which we create using a RelativeResource of 'Self' to get the target,
// and finally, since we have no way of getting the BindingExpressions (as there will be one wherever
// the style is applied), we create a third child binding which is a convenience object on which we
// trigger a change notification, thus refreshing the binding.
var findTargetBinding = new Binding(){
RelativeSource = new RelativeSource(RelativeSourceMode.Self)
};

bindingTrigger = new BindingTrigger();

var wrapperBinding = new MultiBinding(){
Bindings = {
dynamicResourceBinding,
findTargetBinding,
bindingTrigger.Binding
},
Converter = new InlineMultiConverter(WrapperConvert)
};

return wrapperBinding.ProvideValue(serviceProvider);
}

// This gets called on every change of the dynamic resource, for every object it's been applied to
// either when applied directly, or via a style
private object WrapperConvert(object[] values, Type targetType, object parameter, CultureInfo culture) {

var dynamicResourceBindingResult = values[0]; // This is the result of the DynamicResourceBinding**
var bindingTargetObject = values[1]; // The ultimate target of the binding
// We can ignore the bogus third value (in 'values[2]') as that's the dummy result
// of the BindingTrigger's value which will always be 'null'

// ** Note: This value has not yet been passed through the converter, nor been coalesced
// against TargetNullValue, or, if applicable, formatted, both of which we have to do here.
if (Converter != null)
// We pass in the TargetType we're handed here as that's the real target. Child bindings
// would've normally been handed 'object' since their target is the MultiBinding.
dynamicResourceBindingResult = Converter.Convert(dynamicResourceBindingResult, targetType, ConverterParameter, ConverterCulture);

// Check the results for null. If so, assign it to TargetNullValue
// Otherwise, check if the target type is a string, and that there's a StringFormat
// if so, format the string.
// Note: You can't simply put those properties on the MultiBinding as it handles things differently
// than a single binding (i.e. StringFormat is always applied, even when null.
if (dynamicResourceBindingResult == null)
dynamicResourceBindingResult = TargetNullValue;
else if (targetType == typeof(string) && StringFormat != null)
dynamicResourceBindingResult = String.Format(StringFormat, dynamicResourceBindingResult);

// If the binding target object is a FrameworkElement, ensure the BindingSource is added
// to its Resources collection so it will be part of the lookup relative to the FrameworkElement
if (bindingTargetObject is FrameworkElement targetFrameworkElement
&& !targetFrameworkElement.Resources.Contains(bindingSource)) {

// Add the resource to the target object's Resources collection
targetFrameworkElement.Resources[bindingSource] = bindingSource;

// Since we just added the source to the visual tree, we have to re-evaluate the value
// relative to where we are. However, since there's no way to get a binding expression,
// to trigger the binding refresh, here's where we use that BindingTrigger created above
// to trigger a change notification, thus having it refresh the binding with the (possibly)
// new value.
// Note: since we're currently in the Convert method from the current operation,
// we must make the change via a 'Post' call or else we will get results returned
// out of order and the UI won't refresh properly.
SynchronizationContext.Current.Post((state) => {

bindingTrigger.Refresh();

}, null);
}

// Return the now-properly-resolved result of the child binding
return dynamicResourceBindingResult;
}
}

BindingProxy

This is the Freezable mentioned above, but it's also helpful for other binding proxy-related patterns where you need to cross the boundaries of visual trees. Search here or on Google for 'BindingProxy' for more information on that other usage. It's pretty great!

public class BindingProxy : Freezable {

public BindingProxy(){}
public BindingProxy(object value)
=> Value = value;

protected override Freezable CreateInstanceCore()
=> new BindingProxy();

#region Value Property

public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
nameof(Value),
typeof(object),
typeof(BindingProxy),
new FrameworkPropertyMetadata(default));

public object Value {
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}

#endregion Value Property
}

Note: Again, you must use a Freezable for this to work. Inserting any other type of DependencyObject into the target FrameworkElement's resources--ironically even another FrameworkElement--will resolve DynamicResources relative to the Application and not the associated FrameworkElement as non-Freezables in the Resources collection don't participate in localized resource lookup. As a result, you lose any resources which may be defined within the Visual Tree.

BindingTrigger

This class is used to force the MultiBinding to refresh since we don't have access to the ultimate BindingExpression. (Technically you can use any class that supports change notification, but I personally like my designs to be explicit as to their usage.)

public class BindingTrigger : INotifyPropertyChanged {

public BindingTrigger()
=> Binding = new Binding(){
Source = this,
Path = new PropertyPath(nameof(Value))};

public event PropertyChangedEventHandler PropertyChanged;

public Binding Binding { get; }

public void Refresh()
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));

public object Value { get; }
}

InlineMultiConverter

This allows you to set up converters easily in code-behind by simply providing the methods to use for conversion. (I have a similar one for InlineConverter)

public class InlineMultiConverter : IMultiValueConverter {

public delegate object ConvertDelegate (object[] values, Type targetType, object parameter, CultureInfo culture);
public delegate object[] ConvertBackDelegate(object value, Type[] targetTypes, object parameter, CultureInfo culture);

public InlineMultiConverter(ConvertDelegate convert, ConvertBackDelegate convertBack = null){
_convert = convert ?? throw new ArgumentNullException(nameof(convert));
_convertBack = convertBack;
}

private ConvertDelegate _convert { get; }
private ConvertBackDelegate _convertBack { get; }

public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
=> _convert(values, targetType, parameter, culture);

public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
=> (_convertBack != null)
? _convertBack(value, targetTypes, parameter, culture)
: throw new NotImplementedException();
}

Usage

Just like with a regular binding, here's how you use it (assuming you've defined a 'double' resource with the key 'MyResourceKey')...

<TextBlock Text="{drb:DynamicResourceBinding ResourceKey=MyResourceKey, Converter={cv:MultiplyConverter Factor=4}, StringFormat='Four times the resource is {0}'}" />

or even shorter, you can omit 'ResourceKey=' thanks to constructor overloading to match how 'Path' works on a regular binding...

<TextBlock Text="{drb:DynamicResourceBinding MyResourceKey, Converter={cv:MultiplyConverter Factor=4}, StringFormat='Four times the resource is {0}'}" />

So there you have it! Binding to a DynamicResource with full support for converters, string formats, null value handling, etc.!

Anyway, that's it! I really hope this helps other devs as it has really simplified our control templates, especially around common border thicknesses and such.

Enjoy!

WPF Localization: DynamicResource with StringFormat?

So, I finally came up with a solution that allows me to have format strings in my ResourceDictionary and be able to dynamically change the language at runtime. I think it could be improved, but it works.

This class converts the resource key into its value from the ResourceDictionary:

public class Localization
{
public static object GetResource(DependencyObject obj)
{
return (object)obj.GetValue(ResourceProperty);
}

public static void SetResource(DependencyObject obj, object value)
{
obj.SetValue(ResourceProperty, value);
}

// Using a DependencyProperty as the backing store for Resource. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ResourceProperty =
DependencyProperty.RegisterAttached("Resource", typeof(object), typeof(Localization), new PropertyMetadata(null, OnResourceChanged));

private static void OnResourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
//check if ResourceReferenceExpression is already registered
if (d.ReadLocalValue(ResourceProperty).GetType().Name == "ResourceReferenceExpression")
return;

var fe = d as FrameworkElement;
if (fe == null)
return;

//register ResourceReferenceExpression - what DynamicResourceExtension outputs in ProvideValue
fe.SetResourceReference(ResourceProperty, e.NewValue);
}
}

This class allows the value from the ResourceDictionary to be used as the format parameter in String.Format()

public class FormatStringConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (values[0] == DependencyProperty.UnsetValue || values[0] == null)
return String.Empty;

var format = (string)values[0];
var args = values.Where((o, i) => { return i != 0; }).ToArray();

return String.Format(format, args);
}

public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}

Example Usage 1: In this example, I use the FormatStringConverter in the MultiBinding to convert its Binding collection into the desired output. If, for instance, the value of "SomeKey" is "The object id is {0}" and the value of "Id" is "1" then the output will become "The object id is 1".

                <TextBlock ap:Localization.Resource="SomeKey">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource formatStringConverter}">
<Binding Path="(ap:Localization.Resource)" RelativeSource="{RelativeSource Self}" />
<Binding Path="Id" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>

Example Usage 2: In this example, I use a binding with a Converter to change the resource key to something more verbose to prevent key collisions. If, for instance, I have the enum value Enum.Value (displayed by default as "Value"), I use the converter to attach its namespace to make a more unique key. So the value becomes "My.Enums.Namespace.Enum.Value". Then the Text property will resolve with whatever the value of "My.Enums.Namespace.Enum.Value" is in the ResourceDictionary.

        <ComboBox ItemsSource="{Binding Enums}"
SelectedItem="{Binding SelectedEnum}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock ap:Localization.Resource="{Binding Converter={StaticResource enumToResourceKeyConverter}}"
Text="{Binding Path=ap:Localization.Resource), RelativeSource={RelativeSource Self}}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>

Example Usage 3: In this example, the key is a literal and is used only to find its corresponding value in the ResourceDictionary. If, for instance, "SomeKey" has the value "SomeValue" then it will simply output "SomeValue".

                    <TextBlock ap:Localization.Resource="SomeKey"
Text="{Binding Path=ap:Localization.Resource), RelativeSource={RelativeSource Self}}"/>

Binding in DynamicResource ResourceKey wpf mvvm

You cannot use bindings on the DynamicResource properties, as it does not derive from DependencyObject. You would either need to set the Style property directly from code-behind, or just use a Binding.

You could use a Style for the Button type, which has a DataTrigger based on a custom property that dynamically changes the look. But in this case, you need a single Style, which changes it's setters based on your condition. It would not allow you to change the Style property itself dynamically.

How can you set a DynamicResource in code-behind if the target is not a FrameworkElement?

You can use a DynamicResourceExtension instance in your code:

var proxy = new BindingProxy();
var dynamicResourceExtension = new DynamicResourceExtension("TestValue");
proxy.Value = dynamicResourceExtension.ProvideValue(null);

If you see the code reference here you will see that ProvideValue returns a ResourceReferenceExpression when serviceProvider is null. Just almost the same thing that SetResourceReference does



Related Topics



Leave a reply



Submit