How do you create a DynamicResourceBinding that supports Converters, StringFormat?

简介: 原文 How do you create a DynamicResourceBinding that supports Converters, StringFormat? 2 down vote accepted In the past I've resorted to using se...

原文 How do you create a DynamicResourceBinding that supports Converters, StringFormat?

2
down vote
accepted
In the past I've resorted to using several hard-to-follow/hard-to-implement hacks and workarounds to achieve what I feel is missing functionality. That's why to me this is close to (but not quite) the Holy Grail of solutions... being able to use this in XAML exactly like you would a binding, but having the source be a DynamicResource.

Note: I say 'Binding' but technically it's a DynamicResourceExtension on which I've defined a Converter, ConverterParameter, ConverterCulture and StringFormat but which does use a Binding internally. As such, I named it based on its usage model, not its actual type.

The key to making this work is a unique feature of the Freezable class:

If you add a Freezable to the resources of a FrameworkElement, any DependencyProperties on that Freezable which are set to a DynamicResource 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 Freezable's DependencyProperty, add the Freezable to the resource collection of the target FrameworkElement, then use that Freezable's DependencyProperty as the source for an actual binding.

That said, here's the solution.

DynamicResourceBinding
public class DynamicResourceBinding : DynamicResourceExtension
{
    #region Internal Classes

        private class DynamicResourceBindingSource : Freezable
        {
            public static readonly DependencyProperty ResourceReferenceExpressionProperty = DependencyProperty.Register(
                nameof(ResourceReferenceExpression),
                typeof(object),
                typeof(DynamicResourceBindingSource),
                new FrameworkPropertyMetadata());

            public object ResourceReferenceExpression
            {
                get { return GetValue(ResourceReferenceExpressionProperty); }
                set { SetValue(ResourceReferenceExpressionProperty, value); }
            }

            protected override Freezable CreateInstanceCore()
            {
                return new DynamicResourceBindingSource();
            }
        }

    #endregion Internal Classes

    public DynamicResourceBinding(){}

    public DynamicResourceBinding(string resourceKey)
    : base(resourceKey){}

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

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        // Get the expression representing the DynamicResource
        var resourceReferenceExpression = base.ProvideValue(serviceProvider);

        // If there's no converter, nor StringFormat, just return it (Matches standard DynamicResource behavior}
        if(Converter == null && StringFormat == null)
            return resourceReferenceExpression;

        // Create the Freezable-based object and set its ResourceReferenceExpression property directly to the 
        // result of base.ProvideValue (held in resourceReferenceExpression). Then add it to the target FrameworkElement's
        // Resources collection (using itself as its key for uniqueness) so it participates in the resource lookup chain.
        var dynamicResourceBindingSource = new DynamicResourceBindingSource(){ ResourceReferenceExpression = resourceReferenceExpression };

        // Get the target FrameworkElement so we have access to its Resources collection
        // Note: targetFrameworkElement may be null in the case of setters. Still trying to figure out how to handle them.
        // For now, they just fall back to looking up at the app level
        var targetInfo = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
        var targetFrameworkElement = targetInfo.TargetObject as FrameworkElement;
        targetFrameworkElement?.Resources.Add(dynamicResourceBindingSource, dynamicResourceBindingSource);

        // Now since we have a source object which has a DependencyProperty that's set to the value of the
        // DynamicResource we're interested in, we simply use that as the source for a new binding,
        // passing in all of the other binding-related properties.
        var binding = new Binding()
        {
            Path               = new PropertyPath(DynamicResourceBindingSource.ResourceReferenceExpressionProperty),
            Source             = dynamicResourceBindingSource,
            Converter          = Converter,
            ConverterParameter = ConverterParameter,
            ConverterCulture   = ConverterCulture,
            StringFormat       = StringFormat,
            Mode               = BindingMode.OneWay
        };

        // Now we simply return the result of the new binding's ProvideValue
        // method (or the binding itself if the target is not a FrameworkElement)
        return (targetFrameworkElement != null)
            ? binding.ProvideValue(serviceProvider)
            : binding;
    }
}
And 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}'}" />
Awesomesausage! (Well, ok, AwesomeViennasausage thanks to the small 'setter' caveat I uncovered after writing this. I updated the code with the comments.)

As I mentioned, the trick to get this to work is using a Freezable. Thanks to its aforementioned 'magic powers' of participating in the Resource Lookup relative to the target, we can use it as the source of the internal binding where we have full use of all of a binding's facilities.

Note: You must use a Freezable for this to work. Inserting any other type of DependencyObject into the target FrameworkElement's resources--ironically even including another FrameworkElement--will resolve DynamicResources relative to the Application and not the FrameworkElement where you used the DynamicResourceBinding since they don't participate in localized resource lookup (unless they too are in the Visual Tree of course.) As a result, you lose any resources which may be set within the Visual Tree.

The other part of getting that to work is being able to set a DynamicResource on a Freezable from code-behind. Unlike FrameworkElement (which we can't use for the above-mentioned reasons) you can't call SetResourceReference on a Freezable. Actually, I have yet to figure out how to set a DynamicResource on anything but a FrameworkElement.

Fortunately, here we don't have to since the value provided from base.ProvideValue() is the result of such a call anyway, which is why we can simply set it directly to the Freezable's DependencyProperty, then just bind to it.

So there you have it! Binding to a DynamicResource with full Converter and StringFormat support.

For completeness, here's something similar but for StaticResources...

StaticResourceBinding
public class StaticResourceBinding : StaticResourceExtension
{
    public StaticResourceBinding(){}

    public StaticResourceBinding(string resourceKey)
    : base(resourceKey){}

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

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        var staticResourceValue = base.ProvideValue(serviceProvider);

        if(Converter == null)
            return (StringFormat != null)
                ? string.Format(StringFormat, staticResourceValue)
                : staticResourceValue;

        var targetInfo               = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
        var targetFrameworkElement   = (FrameworkElement)targetInfo.TargetObject;
        var targetDependencyProperty = (DependencyProperty)targetInfo.TargetProperty;

        var convertedValue = Converter.Convert(staticResourceValue, targetDependencyProperty.PropertyType, ConverterParameter, ConverterCulture);

        return (StringFormat != null)
            ? string.Format(StringFormat, convertedValue)
            : convertedValue;
    }
}
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!

 

目录
打赏
0
0
0
0
217
分享
相关文章
基于C++开发,支持三维重建,多平面重建技术的医学影像PACS系统源码
支持非DICOM标准的影像设备的图像采集和处理。 3)支持各种扫描仪、数码相机等影像输入设备。 4)支持各大主流厂商的CT、MR、DSA、ECT、US、数字胃肠、内镜等影像设备; 5)支持所有的DICOM相机,支持各大厂家的激光相机。 6)系统完全支持HL7接口和ICD—10编码,可与HIS系统无缝连接。 7)提供全院级、科室级工作站以及远程会诊工作站,三维重建,多平面重建。
248 0
基于C++开发,支持三维重建,多平面重建技术的医学影像PACS系统源码
JavaScript 源代码大放送
JavaScript 不仅是前端开发的首选语言,通过 NodeJS 还能用于开发高性能后端服务,甚至在硬件编程中也崭露头角,正逐步成为全能型语言。此段代码提供了一个自定义日期格式验证的实用函数 `isValidDate`,简化了日期有效性检查,无需依赖庞大的第三方库。此外,还提供了获取元素最大尺寸和高亮文本的函数,以及一个为文字添加动画效果的 jQuery 插件,适用于多种开发场景。
46 2
HTTP调用:你考虑到超时、重试、并发了吗?
今天,我们一起聊聊进行 HTTP 调用需要注意的超时、重试、并发等问题。
470 0
iOS实时查看App运行日志
在移动应用开发过程中,经常需要查看应用在运行时输出的日志信息。而在iOS上,我们可以通过克魔助手提供的功能来实现方便快捷地查看设备上的日志。本文将介绍如何使用克魔助手来实时查看iOS设备上的应用日志。
iOS实时查看App运行日志
C/C++学习 -- Base64算法
C/C++学习 -- Base64算法
154 0
数据库连接的时区问题 The server time zone value is unrecognized
数据库连接的时区问题 The server time zone value is unrecognized
180 0
springboot 启动加载数据库数据到redis缓存
springboot 启动加载数据库数据到redis缓存
544 0
加载秘钥InvalidKeySpecException: java.security.InvalidKeyException: IOException: Short read of DERl 异常处理
加载秘钥InvalidKeySpecException: java.security.InvalidKeyException: IOException: Short read of DERl 异常处理
812 0
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等

登录插画

登录以查看您的控制台资源

管理云资源
状态一览
快捷访问