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

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

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

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

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

输入DynamicResourceBinding

注意:我称之为’绑定’,但从技术上讲,它是一个MarkupExtension ,我在其上定义了属性,如ConverterConverterParameterConverterCulture等,但它最终在内部使用绑定(实际上是几个!)因此,我根据其用途而不是实际类型命名。

但为什么?

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

更好的是,将上面的内容实现为MarkupExtension并简化您的XAML!

     

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

魔术酱

DynamicResourceBinding的实现归功于Freezable数据类型的巧妙技巧。 特别…

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

使用那个“魔术酱”,诀窍是在代理Freezable对象的DependencyProperty上设置DynamicResource ,将Freezable添加到目标FrameworkElement的资源集合中,然后在两者之间设置绑定,现在允许,因为源现在是DependencyObject (即Freezable

复杂性是在Style使用它时获取目标FrameworkElement ,因为MarkupExtension在定义它的位置提供其值,而不是最终应用其结果的位置。 这意味着当您在FrameworkElement上直接使用MarkupExtension时,其目标就是您期望的FrameworkElement 。 但是,在样式中使用MarkupExtension时, 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 (ie 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; } } 

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 } 

注意:同样,您必须使用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; } } 

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

用法

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

  

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

  

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

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

请享用!