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

1

23

Note: This is a revision of an earlier design that had the limitation of not being usable in a style, negating its effectiveness quite a bit. However, this new version now works with styles, essentially letting you use it anywhere you can use a binding or a dynamic resource and get the expected results, making it immensely more useful.

Technically, this isn't a question. It's a post showing a way I found to easily use converters with a DynamicResource as the source, but in order to follow s/o's best practices, I'm posting it as a question/answer pair. So check out my answer below on a way I found how to do this. Hope it helps!

Orola answered 19/11, 2015 at 23:47 Comment(2)
Cool, but I think the preferred way to do this is to ask the question and then answer it yourself.Rawls
Done as requested.Orola
O
20

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.)...

  1. Create a new BindingProxy object (This is simply a Freezable subclass with a single 'Value' DependencyProperty of type Object)
  2. Set its 'Value' property to the DynamicResource you wish to use as the binding source
  3. Add the the BindingProxy to the Resources collection of the target FrameworkElement
  4. 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.)
  5. 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!

Orola answered 7/3, 2018 at 19:6 Comment(14)
Does look better indeed.Fairway
I know you're one of the C#/WPF/XAML heavy-hitters on here as you're constantly responding to my questions. This issue above is a problem I've tried to solve for the past few years and think I finally did yesterday. The only negative I can currently see is every time you use this, you're creating a BindingProxy object, but I'm not really sure that's too much of a negative, especially considering the gains. That said, mind taking a spin through the code to see if anything stands out to you as something that I'm doing wrong, or can be improved, or even if you've come up with your own way?Orola
IMO it's a nuisance that you can't override the sealed ProvideValue method of the Binding class. Deriving from Binding instead of MarkupExtension would greatly simplify it. Besides that, I didn't yet take a closer look. I never had the need to apply a Converter or a StringFormat to a DynamicResource. Could always use them as-is. Also strange is that if you write {Binding Source={DynamicResource ...}} the XAML designer swallows it, but at runtime it throws an exception.Fairway
I'll give you two examples: One, I've defined things like global margins and padding, which normally are applied to all four sides. By creating a subclass of this class, not only can I more easily apply them without having to remember the name of the resource, but I can also add 'edge masks' so I can turn off certain edges of the padding for certain uses. Another case is when I've defined a color as a resource and I need to get 'shades' of it for certain things in the UI. Again, a converter comes to the rescue with the wrapping MarkupExtension using a Shading enum making it cake to apply.Orola
Another one is defining an application-wide font-size, then being able to define fonts in the UI relative to them. For instance, in a list we have a heading and a subheading. By making the subheading a percentage of the size of the resource to use for the heading, any changes to the heading flow to the subheading. That lets a user change the font size for the entire app by simply changing the base size--something our users requested as our app is used outdoors on tablets. Things like that. It really helps with live changes and user-customizations of the 'look' without messing up layout.Orola
This is really awesome and works great, unless I'm binding to a Color resource: <Color x:Key="myKey">#000</Color> <SolidColorBrush x:Key="myBrush" Color="{library:DynamicResourceBinding ResourceKey=myKey}" /> Which gives me "Object reference not set to instance of an object" with an empty call stack. Any ideas?Haematoma
Hmm! I've never had any issues, but I do love a good challenge! Lemme see if I can repro and I'll get back to you. Feel free to hit me up directly if you want. :)Orola
Wow, nice & fast response for a years-old issue. I love this technique so I have my fingers crossed....Haematoma
Haha!! Still looking into this. My suspicion is that you're using one resource from another. I'm not sure my code accounted for that scenario. I only tested this with using it either in a style or directly in a tree, not another resource. This one may require some more in-depth digging, but hey... I love a challenge!Orola
Did you make any progress on this? I love this extension, but System.Windows.Media.Color is not supported, like mentioned above. Could it be because it's a struct and not a class? Brushes and such work just fine. It builds, but crashes at runtime.Hungnam
Well, I think I found the issue. It's due to TargetNullValue not being defined or being defined as {x:Null}. Defining it in XAML or even just returning an empty string from the code instead of null makes it work.Hungnam
Hmm... this gives me an idea. Maybe rather than returning null, I should return default which only returns null for types that support it, or a proper default for types that don't. Let me see what I can come up with.... when I have time! Totally slammed lately, but glad this is still being used! :)Orola
For what it's worth, I had another gander at this. I'm not quite sure where to return default, but couldn't get it to work. What appears to fail, specifically, is this method; referencesource.microsoft.com/#PresentationFramework/src/… For now I just check if TargetNullValue is not null, so it has some kind of value. If it does, I set dynamicResourceBinding.TargetNullValue = TargetNullValue;. Otherwise, I do nothing with dynamicResourceBinding.TargetNullValue = TargetNullValue; and it seems to work as expected.Hungnam
Wait... in both cases you're setting dynamicResourceBinding.TargetNullValue = TargetNullValue;. Is there a difference? And where I meant to use default is in place of null since for reference types, they are the same, but for non-reference types, like an int, it returns something like zero.Orola

© 2022 - 2024 — McMap. All rights reserved.