<!-- Have secondary text be 85% the size of whatever it would normally be at this location in the visual tree -->
<TextBlock Text="Some Primary Text" />
<TextBlock Text="Some secondary text useful for details"
Foreground="Gray"
FontSize="{cwpf:RelativeFontSize 0.85}" />
<!-- Use the app's standard margins, but suppress applying it to the top edge -->
<Border Margin="{cwpf:StandardMargin Mask=1011}" />
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 bindingProxy;
private BindingTrigger bindingTrigger;
public override object ProvideValue(IServiceProvider serviceProvider) {
// Create the BindingProxy for the requested dynamic resource
// This will be used as the source of the underlying binding
var dynamicResource = new DynamicResourceExtension(ResourceKey);
bindingProxy = new BindingProxy(dynamicResource.ProvideValue(null)); // Pass 'null' here
// Set up the actual, underlying binding specifying the just-created
// BindingProxy as its source. Note, we don't yet set the Converter,
// ConverterParameter, StringFormat or TargetNullValue (More on why not below)
var dynamicResourceBinding = new Binding() {
Source = bindingProxy,
Path = new PropertyPath(BindingProxy.ValueProperty),
Mode = BindingMode.OneWay
};
// Get the TargetInfo for this markup extension
var targetInfo = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
// Check if the target object of this markup extension is a DependencyObject.
// If so, we can set up everything right now and we're done!
if(targetInfo.TargetObject is DependencyObject dependencyObject){
// Ok, since we're being applied directly on a DependencyObject, we can
// go ahead and set all the additional binding-related properties.
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
// BindingProxy to its Resources collection to ensure proper resource lookup
// We use itself as it's key so we can check for it's existence later
if (dependencyObject is FrameworkElement targetFrameworkElement)
targetFrameworkElement.Resources[bindingProxy] = bindingProxy;
// And now we simply return the same value as the actual, underlying binding,
// making us mimic being a proper binding, hence the markup extension's name
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 do some extra work 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 child binding with a RelativeResource of 'Self'. During the
// Convert method, we use this to get the ultimate/actual binding target.
//
// Finally, since we have no way of getting the BindingExpression (as there will be a
// separate one for each case where this style is ultimately applied), we create a third
// binding whose only purpose is to manually re-trigger the execution of the 'WrapperConvert'
// method, allowing us to discover the ultimate target via the second child binding above.
// Binding used to find the target this markup extension is ultimately applied to
var findTargetBinding = new Binding(){
RelativeSource = new RelativeSource(RelativeSourceMode.Self)
};
// Binding used to manually 'retrigger' the WrapperConvert method. (See BindingTrigger's implementation)
bindingTrigger = new BindingTrigger();
// Wrapper binding to bring everything together
var wrapperBinding = new MultiBinding(){
Bindings = {
dynamicResourceBinding,
findTargetBinding,
bindingTrigger.Binding
},
Converter = new InlineMultiConverter(WrapperConvert)
};
// Just like above, we return the result of the wrapperBinding's ProvideValue
// call, again making us mimic the behavior of being an actual binding
return wrapperBinding.ProvideValue(serviceProvider);
}
// This gets called on every change of the dynamic resource, for every object this
// markup extension has been applied to, whether 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]; // This is the ultimate target of the binding
// ** Note: This value has not yet been passed through the converter, nor been coalesced
// against TargetNullValue, or, if applicable, formatted, all of which we have to do below.
// We can ignore the third value (i.e. 'values[2]') as that's the result of the bindingTrigger's
// binding, which will always be set to null (See BindingTrigger's implementation for more info)
// Again that binding only exists to re-trigger this WrapperConvert method explicitly when needed.
if (Converter != null)
// We pass in the TargetType we're handed in this method as that's the real binding target.
// Normally, child bindings would been handed 'object' since their target is the MultiBinding.
dynamicResourceBindingResult = Converter.Convert(dynamicResourceBindingResult, targetType, ConverterParameter, ConverterCulture);
// First, check the results for null. If so, set it equal to TargetNullValue and continue
if (dynamicResourceBindingResult == null)
dynamicResourceBindingResult = TargetNullValue;
// It's not null, so check both a) if the target type is a string, and b) that there's a
// StringFormat. If both are true, format the string accordingly.
//
// Note: You can't simply put those properties on the MultiBinding as it handles things
// differently than a regular Binding (e.g. StringFormat is always applied, even when null.)
else if (targetType == typeof(string) && StringFormat != null)
dynamicResourceBindingResult = String.Format(StringFormat, dynamicResourceBindingResult);
// If the binding target object is a FrameworkElement, ensure the binding proxy is added
// to its Resources collection so it will be part of the lookup relative to that element
if (bindingTargetObject is FrameworkElement targetFrameworkElement
&& !targetFrameworkElement.Resources.Contains(bindingProxy)) {
// Add the resource to the target object's Resources collection
targetFrameworkElement.Resources[bindingProxy] = bindingProxy;
// Since we just added the binding proxy to the visual tree, we have to re-evaluate it
// relative to where we now are. However, since there's no way to get a BindingExpression
// to manually refresh it from here, here's where the BindingTrigger created above comes
// into play. By manually forcing a change notification on it's Value property, it will
// retrigger the binding for us, achieving the same thing. However...
//
// Since we're presently executing in the WrapperConvert method from the current binding
// operation, we must retrigger that refresh to occur *after* this execution completes. We
// can do this by putting the refresh code in a closure passed to the 'Post' method on the
// current SynchronizationContext. This schedules that closure to run in the future, as part
// of the normal run-loop cycle. If we didn't schedule this in this way, the results will be
// returned out of order and the UI wouldn't update properly, overwriting the actual values.
// Refresh the binding, but not now, in the future
SynchronizationContext.Current.Post((state) => {
bindingTrigger.Refresh();
}, null);
}
// Return the now-properly-resolved result of the child binding
return dynamicResourceBindingResult;
}
}
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
}
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; }
}
<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}'}" />
1条答案
按热度按时间gdrx4gfi1#
WPF中绑定DynamicResource
TL;DR
警告!这是一篇#$% 长文章!*
我写这篇文章的目的是让那些感兴趣的人深入了解使用
DynamicResource
(或任何MarkupExtension
)时到底发生了什么,为什么这可能在第一次似乎是不可能解决的,以及我证明它的创造性方法,最终导致我找到了下面的工作解决方案。也就是说,如果你只对那个解决方案感兴趣,而不想看到所有的文字碎片,那么你可以自由地向下滚动到标题为“DynamicResourceBinding”的标题,你可以从那里获取相关的代码。
问题
我一直觉得WPF中缺少一些功能:使用
DynamicResource
作为Binding
源的能力。我从技术Angular 理解为什么这是不可能的...在微软的“DynamicResource Markup Extension”documentation的“备注”部分有清楚的解释。上面写着...DynamicResource
将在初始编译期间创建一个临时表达式,从而延迟对资源的查找,直到实际需要所请求的资源值来构造对象。这就是为什么你不能绑定它。它不是一个物体。这甚至不是属性设置的值!它是一个
MarkupExtension
,在初始编译期间,它使用给定的资源键预配置了一个Microsoft内部的ResourceReferenceExpression
,然后通过它的ProvideValue
方法返回该表达式,将其传递给它设置的属性。然后,当有人询问该属性的当前值时,表达式运行,在VisualTree
中的该位置查找具有指定键的资源的当前值,这就是属性返回的值。换句话说,
DynamicResource
不能告诉您资源已更改。这是必须要问的。尽管如此,从概念的Angular 来看,它总是困扰着我,作为可以在运行时动态更改的东西,它应该能够通过转换器进行推送。
好吧,我终于找到了解决这个问题的办法…输入
DynamicResourceBinding
!嗯...但为什么?
乍一看,这似乎是不必要的。毕竟,为什么需要绑定到动态资源?这样做实际上解决了什么?
允许你做一些事情怎么样...
MultiplyByConverter
double
定义应用范围的边距,然后利用DoubleToThicknessConverter
,不仅可以将其转换为厚度,还可以根据需要在布局中屏蔽边缘,从而通过更改应用资源中的单个值来更新整个UI。ThemeColor
,然后使用转换器将其变亮或变暗,甚至根据ColorShadingConverter
的使用情况更改其不透明度。更妙的是,如果您将这些东西 Package 在特定的自定义标记扩展中,您的XAML也会大大简化!这里显示的正是上面的前两个用例,这两个用例都是在我自己的'core.wpf'库中定义的,我现在在我所有的WPF应用程序中使用:
简而言之,这有助于整合主要资源中的所有“基本值”,但它允许您根据需要“调整”它们,而不必在资源集合中手动填充“x”个变量。
神奇的酱汁...
DynamicResourceBinding
之所以能够发挥它的魔力,要归功于Freezable
对象特有的一个鲜为人知的特性。具体来说Freezable
对象添加到FrameworkElement
的Resources
集合中,则通过DynamicResource
设置的该Freezable
对象上的任何依赖项属性的值都将相对于该FrameworkElement在可视树中的位置进行解析**。*如上所述,这是 *
Freezable
对象所特有的 。对于Resources
集合中的所有非Freezable
对象 (讽刺的是还包括其他FrameworkElement
示例!),任何设置的DynamicResource
值都将解析 * 相对于应用程序范围,而不是可视化树中的当前位置 ,这意味着对可视化树中更高位置的资源的任何更改**将基本上被忽略。利用
Freezable
的“魔法酱料”,以下是绑定到DynamicResource
* 所需的步骤(因此您可以使用转换器,FallbackValue等)...1.创建一个新的
BindingProxy
对象 (这只是一个Freezable
子类,带有一个类型为Object
的'Value'DependencyProperty
)1.将其“Value”属性设置为您希望用作绑定源的
DynamicResource
1.将
BindingProxy
添加到目标FrameworkElement
的Resources
集合中1.在目标
DependencyProperty
和BindingProxy
对象的“Value”属性之间设置绑定。* (由于BindingProxy
是Freezable
,它本身是DependencyObject
的子类,因此现在允许这样做。1.指定转换器、字符串格式化程序、空值等。在新的装订上
而这正是
DynamicResourceBinding
自动为您做的!Binding
子类。它是一个MarkupExtension
,我在上面定义了与Binding
相关的属性,如Converter
、ConverterParameter
、ConverterCulture
等。然而,对于大多数语义意图和目的,它在功能上与一个同义,因此被赋予这个名称。(只是不要尝试将它传递给期望真正的Binding
的东西!)*并发症 (也称为有趣的挑战!')
这种方法有一个特别的复杂之处,它真的让我在如何解决这个问题上陷入了一个循环。一致地获取目标
FrameworkElement
,以便我可以将BindingProxy
插入其Resources
集合中。当我直接在一个X1 M55 N1 X上使用X1 M54 N1 X时,它工作得很好,但在一种风格中使用时就坏了。当时我不知道原因,但我后来知道这是因为
MarkupExtension
提供了它的值***它的定义,而不是它的值最终使用的地方。***我假设MarkupExtension的目标总是FrameworkElement,但在样式中使用它的情况下,目标是Style
本身!由于使用了几个内部的“helper”绑定,我也设法绕过了这个限制。如何在评论中解释。
DynamicResourceBinding
细节在笔记里。
绑定代理
这就是上面提到的
Freezable
,它允许DynamicResourceBinding
工作。WPF BindingProxy
,了解有关此类其他用途的更多信息。很不错!*BindingTrigger
这个类是一个简单的'helper class',当你不能访问
BindingExpression
时,它可以手动强制刷新绑定。要做到这一点,您可以将其 Package 为MultiBinding
的子对象沿着您希望刷新的实际绑定,然后通过调用此对象的“Value”属性上的PropertyChanged?.Invoke
来触发刷新。MultiBinding
的一部分的类,但我个人更喜欢我的设计明确其用法,因此创建一个专用的BindingTrigger
示例。InlineMultiConverter
这允许您在代码中设置自定义
MultiValueConverter
,而无需显式创建新类型。它通过指定相关的Convert
/``ConvertBack`方法作为其委托属性来实现这一点。InlineConverter
),将接口更改为IValueConverter
,并相应地更新委托方法的签名。用法
就像使用常规绑定一样,下面是您如何使用它(假设您已经定义了一个'double'资源,其键为'MyResourceKey')...
甚至更短,你可以省略'ResourceKey=',这要归功于构造函数重载,以匹配'Path'在常规绑定上的工作方式。
所以你有它!绑定到
DynamicResource
,完全支持转换器,字符串格式,空值处理等!不管怎样,就是这样!我真的希望这能帮助其他开发者,因为它真的简化了我们的控件模板,特别是在常见的边框厚度等方面。
好好享受吧!