22

注意:这是对早期设计的修订版,其局限性在于无法在样式中使用,从而在很大程度上否定了其有效性。但是,这个新版本现在可以与样式一起使用,基本上让您可以在任何可以使用绑定或动态资源的地方使用它,并获得预期的结果,使其更加有用。

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

4

1 回答 1

14

我一直觉得 WPF 中缺少一些功能:使用动态资源作为绑定源的能力。我从技术上理解为什么会这样——为了检测更改,绑定的源必须是DependencyObject支持的对象或对象上的属性INotifyPropertyChanged,而动态资源实际上是 Microsoft 内部的ResourceReferenceExpression它等同于资源(即它不是一个具有要绑定的属性的对象,更不用说具有更改通知的对象了)——但是,它总是困扰着我,作为可以在运行时更改的东西,它应该能够通过转换器根据需要。

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

输入动态资源绑定

注意:我称它为“绑定”,但从技术上讲,它是MarkupExtension我定义了诸如ConverterConverterParameterConverterCulture等属性的一个,但它最终在内部使用了一个绑定(实际上是几个!)因此,我将它命名为基于关于它的用法,而不是它的实际类型。

但为什么?

那么为什么你甚至需要这样做呢?如何根据用户偏好全局缩放字体大小,同时仍然能够利用相对字体大小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)" />

简而言之,这有助于整合主要资源中的所有“基本值”,但可以在何时何地调整它们,而不必在资源集合中填充“x”个变体。

魔法酱

的实现DynamicResourceBinding要归功于Freezable数据类型的巧妙技巧。具体来说...

如果将 Freezable 添加到 FrameworkElement 的 Resources 集合,则该 Freezable 对象上设置为动态资源的任何依赖属性都将解析这些资源,这些资源相对于该 FrameworkElement 在可视树中的位置。

使用那一点“魔法酱”,诀窍是在代理对象的 aDynamicResource上设置 a ,将其添加到 target 的资源集合中,然后在两者之间建立绑定,现在允许这样做,因为源是现在一个(即一个。)DependencyPropertyFreezableFreezableFrameworkElementDependencyObjectFreezable

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

也就是说,这是内联注释的解决方案:

动态资源绑定

“魔法酱!” 阅读内联评论以了解正在发生的事情

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;
    }
}

绑定代理

这是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
}

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

绑定触发器

此类用于强制MultiBinding刷新,因为我们无权访问 Ultimate 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; }
}

内联多转换器

这允许您通过简单地提供用于转换的方法,在代码隐藏中轻松设置转换器。(我有一个类似的 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();
}

用法

就像使用常规绑定一样,这是您使用它的方式(假设您已经使用键“MyResourceKey”定义了一个“双”资源)...

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

甚至更短,由于构造函数重载以匹配“路径”在常规绑定上的工作方式,您可以省略“ResourceKey =”...

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

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

无论如何,就是这样!我真的希望这对其他开发人员有所帮助,因为它确实简化了我们的控制模板,尤其是在常见的边框厚度等方面。

享受!

于 2018-03-07T19:06:31.553 回答