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

时间:2015-11-19 23:47:31

标签: c# wpf ivalueconverter dynamicresource

  

注意:这是对早期设计的修订版,其限制在一种风格中不可用,否则其效果相当大。但是,这个新版本现在可以使用样式,实际上让您可以在任何可以使用绑定或动态资源的地方使用它,并获得预期的结果,从而使它变得非常有用。

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

1 个答案:

答案 0 :(得分:6)

我一直觉得WPF中缺少一些功能:使用动态资源作为绑定源的能力。我从技术上理解为什么会这样 - 为了检测更改,绑定的来源必须是DependencyObject或支持INotifyPropertyChanged的对象的属性,动态资源实际上是Microsoft内部ResourceReferenceExpression等同于资源的(即它不是具有要绑定的属性的对象,更不用说具有更改通知的对象) - 但是尽管如此,它始终告诉我,在运行期间可以改变的东西,它应该能够根据需要推送到转换器。

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

输入 DynamicResourceBinding

注意:我称之为'绑定'但从技术上来说,我MarkupExtension定义了ConverterConverterParameterConverterCulture等属性,但最终使用了内部绑定(实际上是几个!)因此,我根据其用法而不是实际类型对其进行了命名。

但为什么?

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

更好的是,将上面的内容实现为MarkupExtension,您的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”&#39; x&#39;资源集合中的变体数量。

魔术酱

DynamicResourceBinding的实现归功于Freezable数据类型的巧妙处理。具体地说...

  

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

使用魔法酱的那一点,诀窍是在DynamicResource代理DependencyProperty对象上设置Freezable,添加{{1}到目标Freezable的资源集合,然后在两者之间设置绑定,现在允许,因为源现在是FrameworkElement(即DependencyObject。)

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

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

DynamicResourceBinding

魔术酱!&#39;阅读内容评论了解

的内容
FrameworkElement

BindingProxy

这是上面提到的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; } } ,但它对于需要跨越可视树的边界的其他绑定代理相关模式也很有帮助。在此处或在Google上搜索&#39; BindingProxy&#39;有关其他用法的更多信息。太棒了!

Freezable

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

BindingTrigger

此类用于强制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 } 刷新,因为我们无法访问最终的MultiBinding(从技术上讲,您可以使用任何支持更改通知的类,但我个人喜欢我的设计明确其用法。)

BindingExpression

InlineMultiConverter

这使您可以通过简单地提供用于转换的方法在代码隐藏中轻松设置转换器。 (我有一个类似于InlineConverter的人)

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

用法

就像常规绑定一样,这里有你如何使用它(假设你已经使用密钥&#39; MyResourceKey&#39;来定义了一个&#39; double&#39;资源)。 ..

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

甚至更短,您可以省略&#39; ResourceKey =&#39;感谢构造函数重载以匹配&#39; Path&#39;适用于常规绑定......

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

所以你有它!绑定到<TextBlock Text="{drb:DynamicResourceBinding MyResourceKey, Converter={cv:MultiplyConverter Factor=4}, StringFormat='Four times the resource is {0}'}" /> ,完全支持转换器,字符串格式,空值处理等等!

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

享受!