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

问题描述 投票:14回答:1

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

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

c# wpf ivalueconverter dynamicresource
1个回答
6
投票

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

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

输入DynamicResource Binding!

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

但为什么?

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

更好的是,实现上面的ColorShadingConverters,你的XAML也被简化了!

MarkupExtension

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

魔术酱

<!-- 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)" /> 的实现归功于DynamicResourceBinding数据类型的巧妙技巧。特别...

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

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

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

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

DynamicResource绑定

'魔术酱!'阅读内联评论,了解正在发生的事情

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上搜索“BindingProxy”以获取有关其他用法的更多信息。真是太棒了!

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

用法

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

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

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

<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}'}" /> 完全支持转换器,字符串格式,空值处理等!

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

请享用!

© www.soinside.com 2019 - 2024. All rights reserved.