Binding to a DynamicResource in WPF
TL;DR
Warning! This is one #$%* long post!
I wrote this post with the intent of taking those interested on a deep dive into understanding exactly what's going on under the hood when using a DynamicResource
(or any MarkupExtension
for that matter), why this may at first seem impossible to solve, and the creative ways I proved it could that ultimately led me to the working solution as presented below.
That said, if you're only interested in that solution without all the verbal detritus, feel free to scroll down to the heading titled DynamicResourceBinding
and you can grab the relevant code from there.
The Issue
There's something I've always felt was a bit of missing functionality in WPF: the ability to use a DynamicResource
as the source of a Binding
. I understand from a technical perspective why this isn't possible... it's clearly explained in the Remarks section of Microsoft's 'DynamicResource Markup Extension' documentation. There it states...
A DynamicResource
will create a temporary expression during the initial compilation and thus defer lookup for resources until the requested resource value is actually required in order to construct an object.
And that's why you can't bind to it. It's not an object. It's not even what the property gets set to! It's a MarkupExtension
that during it's initial compilation preconfigures a Microsoft-internal ResourceReferenceExpression
with the given resource key, then returns that expression via it's ProvideValue
method, handing it off to the property it's set on. Then later, when someone asks that property for it's current value, the expression runs, looking up the current value for the resource with the specified key at that location in the VisualTree
, and that's the value which gets returned by the property.
In other words, a DynamicResource
can't tell you the resource changed. It has to be asked.
Still, from a conceptual standpoint, it's always bugged me that as something that can dynamically change at run-time, it should be able to be pushed through a converter.
Well, I've finally figured out a solution to exactly this omission... enter the DynamicResourceBinding
!
Um... but why?
Upon first glance, this seems like an unnecessity. After all, why would you need to bind to a dynamic resource? What does doing so actually solve?
How about allowing you to do things like...
- Globally scaling your font size based on a user preference or accessibility features stored in a resource while still being able to utilize relative font sizes in your UI thanks to a
MultiplyByConverter
- Defining app-wide margins based simply on a
double
, then leveraging a DoubleToThicknessConverter
that not only converts it to a thickness, but lets you mask out edges as needed in the layout letting you update your entire UI by changing a single value in your app resources.
- Defining a single base
ThemeColor
in a resource, then using a converter to lighten or darken it, or even change its opacity depending on usage thanks to a ColorShadingConverter
Even better, if you wrap such things in specific, custom markup extensions, your XAML is greatly simplified too! Here is showing exactly that for the first two use-cases above, both of which are defined in my own 'core.wpf' library that I now use in all my WPF applications:
<!-- Have secondary text be 85% the size of whatever it would normally be at this location in the visual tree -->
<TextBlock Text="Some Primary Text" />
<TextBlock Text="Some secondary text useful for details"
Foreground="Gray"
FontSize="{cwpf:RelativeFontSize 0.85}" />
<!-- Use the app's standard margins, but suppress applying it to the top edge -->
<Border Margin="{cwpf:StandardMargin Mask=1011}" />
In short, this helps consolidate all the 'base values' in your main resources, but it allows you to 'tweak' them on an as-needed basis without having to manually cram 'x' number of variations to them in your resources collection.
The 'Magic Sauce'...
The ability of DynamicResourceBinding
to work it's magic is thanks to a little-known feature that is unique to Freezable
objects. Specifically...
If you add a Freezable
object to the Resources
collection of a FrameworkElement
, any dependency properties on that Freezable
object which are set via a DynamicResource
will have their values resolved relative to that FrameworkElement's position in the Visual Tree.
As mentioned above, this is unique to Freezable
objects. For all non-Freezable
objects in the Resources
collection (ironically also including other FrameworkElement
instances!), any set DynamicResource
values will resolve relative to the application scope, not the current location in the visual tree, meaning any changes to that resource further up in the visual tree will essentially be ignored.
Leveraging that bit of 'magic sauce' from Freezable
, here are the steps needed to bind to a DynamicResource
(so you can use a converter, FallbackValue, etc.)...
- Create a new
BindingProxy
object (This is simply a Freezable
subclass with a single 'Value' DependencyProperty
of type Object
)
- Set its 'Value' property to the
DynamicResource
you wish to use as the binding source
- Add the the
BindingProxy
to the Resources
collection of the target FrameworkElement
- Set up a binding between the target
DependencyProperty
and the 'Value' property of the BindingProxy
object. (Since BindingProxy
is a Freezable
, which itself is a subclass of DependencyObject
, this is now allowed.)
- Specify your converters, string formatters, null values, etc. on the new binding
And that's exactly what DynamicResourceBinding
does for you automatically!
Note: While it has the name 'DynamicResourceBinding', it's not actually a Binding
subclass. It's a MarkupExtension
on which I've defined correlating Binding
-related properties such as Converter
, ConverterParameter
, ConverterCulture
, etc. However, for most semantic intents and purposes, it's functionally synonymous with one, hence being given that name. (Just don't try passing it to something expecting a true Binding
!)
Complications (a.k.a. 'Fun Challenges!')
There was one particular complication with this approach that really threw me for a loop on how to solve... consistently getting the target FrameworkElement
so I could insert the BindingProxy
into its Resources
collection. It worked fine when I used the DynamicResourceBinding
directly on a FrameworkElement
, but it broke when using it in a style.
At the time I didn't know the reason why, but I've since learned it's because a MarkupExtension
provides its value where it's defined, not where its value is ultimately used. I was assuming the target of a MarkupExtension was always the FrameworkElement, but in the case of using it in a style, the target was the Style
itself!
Thanks to the use of several internal 'helper' bindings, I've managed to get around this limitation as well. How is explained in the comments.
DynamicResourceBinding
Details are in the notes.
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 bindingProxy;
private BindingTrigger bindingTrigger;
public override object ProvideValue(IServiceProvider serviceProvider) {
// Create the BindingProxy for the requested dynamic resource
// This will be used as the source of the underlying binding
var dynamicResource = new DynamicResourceExtension(ResourceKey);
bindingProxy = new BindingProxy(dynamicResource.ProvideValue(null)); // Pass 'null' here
// Set up the actual, underlying binding specifying the just-created
// BindingProxy as its source. Note, we don't yet set the Converter,
// ConverterParameter, StringFormat or TargetNullValue (More on why not below)
var dynamicResourceBinding = new Binding() {
Source = bindingProxy,
Path = new PropertyPath(BindingProxy.ValueProperty),
Mode = BindingMode.OneWay
};
// Get the TargetInfo for this markup extension
var targetInfo = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
// Check if the target object of this markup extension is a DependencyObject.
// If so, we can set up everything right now and we're done!
if(targetInfo.TargetObject is DependencyObject dependencyObject){
// Ok, since we're being applied directly on a DependencyObject, we can
// go ahead and set all the additional binding-related properties.
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
// BindingProxy to its Resources collection to ensure proper resource lookup.
// We use itself as the Resources key so we can easily check for it's existence in the Resources collection later
if (dependencyObject is FrameworkElement targetFrameworkElement)
targetFrameworkElement.Resources[bindingProxy] = bindingProxy;
// And now we simply return the same value as the actual, underlying binding,
// making us mimic being a proper binding, hence the markup extension's name
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 do some extra work 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 'findTargetBinding' with a RelativeSource of 'Self'. During the
// Convert method, we use this binding's value to get the ultimate/actual binding target.
//
// Finally, since we have no way of getting the BindingExpression (as there will be a
// separate one for each case where this style is ultimately applied), we create a third
// binding whose only purpose is to manually re-trigger the execution of the wrapperBinding's Convert
// method, thus allowing us to get the ultimate target via the value of the 'findTargetBinding' as mentioned above.
// Binding used to find the target this markup extension is ultimately applied to
var findTargetBinding = new Binding(){
RelativeSource = new RelativeSource(RelativeSourceMode.Self)
};
// Binding used to manually 'retrigger' the WrapperConvert method. (See BindingTrigger's implementation)
bindingTrigger = new BindingTrigger();
// Wrapper binding to bring everything together
var wrapperBinding = new MultiBinding(){
Bindings = {
dynamicResourceBinding,
findTargetBinding,
bindingTrigger.Binding
},
Converter = new InlineMultiConverter(WrapperConvert)
};
// Just like above, we return the result of the wrapperBinding's ProvideValue
// call, again making us mimic the behavior of being an actual binding
return wrapperBinding.ProvideValue(serviceProvider);
}
// This gets called on every change of the dynamic resource, for every object this
// markup extension has been applied to, whether 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]; // This is the ultimate target of the binding
// ** Note: This value has not yet been passed through the converter, nor been coalesced
// against TargetNullValue, or, if applicable, formatted, all of which we have to do below.
// We can ignore the third value (i.e. 'values[2]') as that's the result of the bindingTrigger's
// binding, which will always be set to null (See BindingTrigger's implementation for more info)
// Again that binding only exists to re-trigger this WrapperConvert method explicitly when needed.
if (Converter != null)
// We pass in the TargetType we're handed in this method as that's the real binding target.
// Normally, child bindings would been handed 'object' since their target is the MultiBinding.
dynamicResourceBindingResult = Converter.Convert(dynamicResourceBindingResult, targetType, ConverterParameter, ConverterCulture);
// First, check the results for null. If so, set it equal to TargetNullValue and continue
if (dynamicResourceBindingResult == null)
dynamicResourceBindingResult = TargetNullValue;
// It's not null, so check both a) if the target type is a string, and b) that there's a
// StringFormat. If both are true, format the string accordingly.
//
// Note: You can't simply put those properties on the MultiBinding as it handles things
// differently than a regular Binding (e.g. StringFormat is always applied, even when null.)
else if (targetType == typeof(string) && StringFormat != null)
dynamicResourceBindingResult = String.Format(StringFormat, dynamicResourceBindingResult);
// If the binding target object is a FrameworkElement, ensure the binding proxy is added
// to its Resources collection so it will be part of the lookup relative to that element
if (bindingTargetObject is FrameworkElement targetFrameworkElement
&& !targetFrameworkElement.Resources.Contains(bindingProxy)) {
// Add the resource to the target object's Resources collection
targetFrameworkElement.Resources[bindingProxy] = bindingProxy;
// Since we just added the binding proxy to the visual tree, we have to re-evaluate it
// relative to where we now are. However, since there's no way to get a BindingExpression
// to manually refresh it from here, here's where the BindingTrigger created above comes
// into play. By manually forcing a change notification on it's Value property, it will
// retrigger the binding for us, achieving the same thing. However...
//
// Since we're presently executing in the WrapperConvert method from the current binding
// operation, we must retrigger that refresh to occur *after* this execution completes. We
// can do this by putting the refresh code in a closure passed to the 'Post' method on the
// current SynchronizationContext. This schedules that closure to run in the future, as part
// of the normal run-loop cycle. If we didn't schedule this in this way, the results will be
// returned out of order and the UI wouldn't update properly, overwriting the actual values.
// Refresh the binding, but not now, in the future
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 which allows the DynamicResourceBinding
to work.
Note: This is also pretty helpful for some other binding-proxy-related patterns where you need to cross the boundaries of visual trees, such as setting up bindings in tool tips or dropdown menus, hence why it's separated out into its own object for reusability. Search here or on Google for WPF BindingProxy
for more information on such 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
}
BindingTrigger
This class is a simple 'helper class' used to manually force a binding to refresh when you don't have access to its BindingExpression
. You do this by wrapping it as a child of a MultiBinding
along with the actual binding you wish to refresh, then trigger that refresh via calling PropertyChanged?.Invoke
on this object's 'Value' property.
Note: Technically you can use any class that supports change notification, including one you may already have configured as part of the MultiBinding
, but I personally prefer my designs to be explicit as to their usage, hence creating a dedicated BindingTrigger
instance.)
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 a custom MultiValueConverter
in code without having to explicitly create a new type. It does this by specifying the relevant Convert
/``ConvertBack` methods as delegate properties on it.
Note: You can create the correlating version representing a standard value converter. Simply give it a new name (like InlineConverter
), change the interface to IValueConverter
, and update the signatures of the delegate methods accordingly.
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}'}" />
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!