Note: This is a revision of an earlier design that had the limitation of not being usable in a style, negating its effectiveness quite a bit. However, this new version now works with styles, essentially letting you use it anywhere you can use a binding or a dynamic resource and get the expected results, making it immensely more useful.
从技术上讲,这不是一个问题.这是一篇文章,展示了我发现可以轻松使用带有DynamicResource的转换器作为源的方法,但为了遵循s / o的最佳实践,我将其作为问题/答案对发布.所以我在下面找到了如何做到这一点的方法,请查看我的答案.希望能帮助到你!
我一直觉得 WPF中缺少一些功能:使用动态资源作为绑定源的能力.我从技术上理解为什么会这样 – 为了检测更改,绑定的源必须是DependencyObject或支持INotifyPropertyChanged的对象的属性,而动态资源实际上是Microsoft内部的ResourceReferenceExpression,它等同于资源的价值(即它不是具有要绑定的属性的对象,更不用说带有更改通知的对象) – 但是,它总是让我觉得,因为在运行时可以改变的东西,它应该能够根据需要推动转换器.好吧,我相信我终于纠正了这个限制……
输入DynamicResourceBinding!
注意:我称之为’绑定’,但从技术上讲,它是一个MarkupExtension,我在其上定义了属性,如Converter,ConverterParameter,ConverterCulture等,但它最终在内部使用绑定(实际上是几个!)因此,我根据其用途命名,而不是实际类型.
但为什么?
那为什么你甚至需要这样做呢?如何通过MultiplyByConverter根据用户偏好全局缩放字体大小,同时仍然能够利用相对字体大小?或者如何通过使用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数据类型的巧妙技巧.特别…
If you add a Freezable to the Resources collection of a FrameworkElement, any dependency properties on that Freezable object which are set as dynamic resources will resolve those resources relative to that FrameworkElement’s position in the 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 (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; } }
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’资源)…
<TextBlock Text="{drb:DynamicResourceBinding ResourceKey=MyResourceKey, Converter={cv:MultiplyConverter Factor=4}, StringFormat='Four times the resource is {0}'}" />
甚至更短,你可以省略’ResourceKey =’,这要归功于构造函数重载以匹配’Path’在常规绑定上的工作方式……
<TextBlock Text="{drb:DynamicResourceBinding MyResourceKey, Converter={cv:MultiplyConverter Factor=4}, StringFormat='Four times the resource is {0}'}" />
所以你有它!绑定到DynamicResource,完全支持转换器,字符串格式,空值处理等!
无论如何,就是这样!我真的希望这有助于其他开发人员,因为它真正简化了我们的控制模板,特别是在常见的边框厚度等方面.
请享用!