Ответ 1
Там что-то, что я всегда чувствовал, было немного недостающей функциональностью в WPF: возможность использовать динамический ресурс в качестве источника привязки. Я понимаю технически, почему это так - для обнаружения изменений источник привязки должен быть свойством в DependencyObject
или объекте, который поддерживает INotifyPropertyChanged
, а динамический ресурс на самом деле является внутренним выражением ResourceReferenceExpression
которое соответствует значение ресурса (т.е. это не объект с привязкой к свойству, не говоря уже об одном из уведомлений об изменении) --but, все же он всегда искажал меня, что как что-то, что может измениться во время выполнения, оно должно быть в состоянии проталкивается через конвертер по мере необходимости.
Ну, я считаю, что я, наконец, исправил это ограничение...
Введите DynamicResourceBinding !
Примечание: я называю это " MarkupExtension
", но технически это MarkupExtension
на котором я определил такие свойства, как Converter
, ConverterParameter
, ConverterCulture
и т.д., Но который в конечном счете использует внутреннюю привязку (несколько, фактически!). Таким образом, я назвали его на основе его использования, а не его фактического типа.
Но почему?
Так зачем вам это нужно? Как глобально масштабировать размер шрифта на основе предпочтений пользователя, сохраняя при этом возможность использовать относительные размеры шрифтов благодаря MultiplyByConverter
? Или как определить границы полей приложения, основанные просто на double
ресурсе, используя DoubleToThicknessConverter
который не только преобразует его в толщину, но позволяет маскировать края по мере необходимости в макете? Или как определить базовый ThemeColor
в ресурсе, а затем использовать конвертер, чтобы осветлить или затемнить его, или изменить его непрозрачность в зависимости от использования благодаря ColorShadingConverter
?
Еще лучше, реализовать выше как MarkupExtension
и ваш 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 в Resource Framework элемента Framework, любые свойства зависимостей для этого объекта Freezable, заданные как динамические ресурсы, будут разрешать эти ресурсы относительно этой позиции FrameworkElement в визуальном дереве.
Используя этот бит "волшебного соуса", трюк заключается в том, чтобы установить DynamicResource
на DependencyProperty
прокси-объекта Freezable
, добавить это Freezable
в коллекцию ресурсов целевого элемента FrameworkElement
, а затем установить привязку между двумя, что теперь разрешено, так как источник теперь является DependencyObject
(т.е. Freezable
.)
Сложность заключается в получении целевого MarkupExtension
FrameworkElement
при использовании этого в Style
, поскольку MarkupExtension
предоставляет свое значение там, где оно определено, а не где его результат в конечном итоге применяется. Это означает, что когда вы используете MarkupExtension
непосредственно в FrameworkElement
, его целью является 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 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 the dummy result
// of the BindingTrigger 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 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 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 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 no way to get a binding expression,
// to trigger the binding refresh, here 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 - разрешит DynamicResources относительно приложения, а не связанный с ним FrameworkElement, поскольку не-Freezables в коллекции ресурсов не участвуют в локальном поиске ресурсов. В результате вы теряете любые ресурсы, которые могут быть определены в визуальном дереве.
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")...
<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
с полной поддержкой конвертеров, строковых форматов, обработки нулевого значения и т.д.!
Во всяком случае, это! Я действительно надеюсь, что это поможет другим разработчикам, поскольку это действительно упростило наши шаблоны управления, особенно вокруг общих границ границ и т.д.
Наслаждайтесь!