注意:这是对早期设计的修订版,其局限性在于无法在样式中使用,从而在很大程度上否定了其有效性。但是,这个新版本现在可以与样式一起使用,基本上让您可以在任何可以使用绑定或动态资源的地方使用它,并获得预期的结果,使其更加有用。
从技术上讲,这不是问题。这是一篇文章,展示了一种我发现可以轻松使用以 aDynamicResource
作为源的转换器的方法,但为了遵循 s/o 的最佳实践,我将其作为问答对发布。所以看看我下面的答案,我发现了如何做到这一点。希望能帮助到你!
注意:这是对早期设计的修订版,其局限性在于无法在样式中使用,从而在很大程度上否定了其有效性。但是,这个新版本现在可以与样式一起使用,基本上让您可以在任何可以使用绑定或动态资源的地方使用它,并获得预期的结果,使其更加有用。
从技术上讲,这不是问题。这是一篇文章,展示了一种我发现可以轻松使用以 aDynamicResource
作为源的转换器的方法,但为了遵循 s/o 的最佳实践,我将其作为问答对发布。所以看看我下面的答案,我发现了如何做到这一点。希望能帮助到你!
我一直觉得 WPF 中缺少一些功能:使用动态资源作为绑定源的能力。我从技术上理解为什么会这样——为了检测更改,绑定的源必须是DependencyObject
支持的对象或对象上的属性INotifyPropertyChanged
,而动态资源实际上是 Microsoft 内部的,ResourceReferenceExpression
它等同于资源(即它不是一个具有要绑定的属性的对象,更不用说具有更改通知的对象了)——但是,它总是困扰着我,作为可以在运行时更改的东西,它应该能够通过转换器根据需要。
好吧,我相信我终于纠正了这个限制......
输入动态资源绑定!
注意:我称它为“绑定”,但从技术上讲,它是MarkupExtension
我定义了诸如Converter
、ConverterParameter
、ConverterCulture
等属性的一个,但它最终在内部使用了一个绑定(实际上是几个!)因此,我将它命名为基于关于它的用法,而不是它的实际类型。
那么为什么你甚至需要这样做呢?如何根据用户偏好全局缩放字体大小,同时仍然能够利用相对字体大小MultiplyByConverter
?或者如何简单地基于double
资源定义应用程序范围的边距,方法是使用DoubleToThicknessConverter
不仅将其转换为厚度,还可以根据需要在布局中屏蔽边缘?或者如何ThemeColor
在资源中定义一个基础,然后使用转换器使其变亮或变暗,或者根据使用情况更改其不透明度,这要归功于ColorShadingConverter
?
更好的是,将上述实现为MarkupExtension
s 并且您的 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 的资源集合中,然后在两者之间建立绑定,现在允许这样做,因为源是现在一个(即一个。)DependencyProperty
Freezable
Freezable
FrameworkElement
DependencyObject
Freezable
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
完全支持转换器、字符串格式、空值处理等!
无论如何,就是这样!我真的希望这对其他开发人员有所帮助,因为它确实简化了我们的控制模板,尤其是在常见的边框厚度等方面。
享受!