如何绑定到DynamicResource以便可以使用Converter或StringFormat等?(修订版4)

Mar*_*eIV 15 c# wpf ivalueconverter dynamicresource

注意:这是对早期设计的修订,其具有不可用于样式的限制,相当于否定其有效性.但是,这个新版本现在可以使用样式,基本上可以让你在任何可以使用绑定或动态资源的地方使用它,并获得预期的结果,使它非常有用.

从技术上讲,这不是一个问题.这是一篇文章,展示了我发现轻松使用转换器DynamicResource作为源的方式,但为了遵循s/o的最佳实践,我将其作为问题/答案对发布.所以我在下面找到了如何做到这一点的方法,看看我的答案.希望能帮助到你!

Mar*_*eIV 8

我一直觉得WPF中缺少一些功能:使用动态资源作为绑定源的能力.我明白为什么在技术上这是-为了检测更改,绑定的源必须是对财产DependencyObject或支持的对象上INotifyPropertyChanged,并且动态资源实际上是微软内部ResourceReferenceExpression,其等同于价值的资源(即它不是具有要绑定的属性的对象,更不用说具有更改通知的对象) - 但是,它始终告诉我,作为可以在运行时更改的内容,它应该能够通过转换器根据需要.

好吧,我相信我终于纠正了这个限制......

输入DynamicResourceBinding!

注:我把它叫做"绑定",但在技术上这是一个MarkupExtension关于这一点我已经定义如属性Converter,ConverterParameter,ConverterCulture(!几个,其实)等,但这个最终使用绑定在内部,因此,我有它基于命名关于它的用法,而不是它的实际类型.

但为什么?

那为什么你甚至需要这样做呢?如何通过基于用户偏好全局缩放字体大小,同时仍然能够利用相对字体大小,这要归功于MultiplyByConverter?或者如何仅仅根据double资源定义应用程序范围的边距DoubleToThicknessConverter,不仅可以将其转换为厚度,还可以根据需要在布局中屏蔽边缘?或者如何ThemeColor在资源中定义基础,然后使用转换器使其变亮或变暗,或者根据使用情况改变其不透明度,这要归功于ColorShadingConverter

更好的是,将上面的内容实现为MarkupExtensions,您的XAML也得到了简化!

<!-- 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)" />
Run Code Online (Sandbox Code Playgroud)

简而言之,这有助于整合主要资源中的所有"基本值",但能够在使用它们的时间和地点进行调整,而无需在资源集合中填充"x"数量的变体.

魔术酱

实现DynamicResourceBinding是由于Freezable数据类型的巧妙技巧.特别...

如果将Freezable添加到FrameworkElement的Resources集合中,则Freezable对象上设置为动态资源的任何依赖项属性都将解析相对于FrameworkElement在Visual Tree中的位置的那些资源.

使用"魔汁"的那一点,关键是要设置DynamicResource一个DependencyProperty代理的Freezable对象,添加Freezable到目标的资源集合FrameworkElement,然后设置了两个,这是现在允许之间的结合,因为源是现在a DependencyObject(即a Freezable.)

FrameworkElement当在a中使用它时,复杂性是获得目标Style,因为MarkupExtension它在定义的位置提供其值,而不是最终应用其结果的位置.这意味着当您MarkupExtension直接使用a时FrameworkElement,其目标就是FrameworkElement您所期望的.但是,MarkupExtension在样式中使用a 时,Style对象是其目标,而MarkupExtension不是FrameworkElement应用的位置.由于使用了第二个内部绑定,我设法绕过了这个限制.

也就是说,这是内联评论的解决方案:

DynamicResourceBinding

'魔术酱!' 阅读内联评论,了解正在发生的事情

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

BindingProxy

这是Freezable上面提到的,但它也有助于其他绑定代理相关的模式,您需要跨越可视树的边界.在此处或在Google上搜索"BindingProxy"以获取有关其他用法的更多信息.真是太棒了!

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
}
Run Code Online (Sandbox Code Playgroud)

注意:同样,您必须使用Freezable才能工作.将任何其他类型的DependencyObject插入到目标FrameworkElement的资源中 - 具有讽刺意味的是甚至是另一个FrameworkElement - 将解析相对于Application的DynamicResources而不是关联的FrameworkElement,因为Resources集合中的非Freezables不参与本地化资源查找.因此,您将丢失可能在Visual Tree中定义的任何资源.

BindingTrigger

此类用于强制MultiBinding刷新,因为我们无法访问终极版BindingExpression.(从技术上讲,您可以使用任何支持更改通知的类,但我个人喜欢我的设计,以明确它们的用法.)

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; }
}
Run Code Online (Sandbox Code Playgroud)

InlineMultiConverter

这使您可以通过简单地提供用于转换的方法在代码隐藏中轻松设置转换器.(我有一个类似于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();
}
Run Code Online (Sandbox Code Playgroud)

用法

就像使用常规绑定一样,这里是你如何使用它(假设你已经使用键'MyResourceKey'定义了'double'资源)...

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

甚至更短,你可以省略'ResourceKey =',这要归功于构造函数重载以匹配'Path'在常规绑定上的工作方式......

<TextBlock Text="{drb:DynamicResourceBinding MyResourceKey, Converter={cv:MultiplyConverter Factor=4}, StringFormat='Four times the resource is {0}'}" />
Run Code Online (Sandbox Code Playgroud)

所以你有它!绑定到DynamicResource完全支持转换器,字符串格式,空值处理等!

无论如何,就是这样!我真的希望这有助于其他开发人员,因为它真正简化了我们的控制模板,特别是在常见的边框厚度等方面.

请享用!

  • 我知道您是这里的 C#/WPF/XAML 重量级人物之一,因为您不断回答我的问题。上面的这个问题是我过去几年一直试图解决的问题,我想我昨天终于解决了。我目前看到的唯一负面影响是每次使用它时,都会创建一个“BindingProxy”对象,但我不确定这是否有太多负面影响,特别是考虑到收益。也就是说,介意浏览一下代码,看看是否有什么地方是我做错的,或者可以改进的,或者即使你想出了自己的方法? (2认同)
  • IMO,您不能覆盖Binding类的密封的ProvideValue方法是很麻烦的。从Binding而不是MarkupExtension派生会大大简化它。除此之外,我还没有仔细观察。我从来不需要将Converter或StringFormat应用于DynamicResource。可以始终按原样使用它们。同样奇怪的是,如果您编写了{Binding Source = {DynamicResource ...}},XAML设计器会吞下它,但是在运行时会抛出异常。 (2认同)