Подтвердить что ты не робот

Как вы можете привязать к DynamicResource, чтобы вы могли использовать конвертер или StringFormat и т.д.? (Редакция 4)

Примечание. Это пересмотр более раннего дизайна, в котором было ограничено отсутствие использования в стиле, что отрицательно сказывалось на его эффективности. Тем не менее, эта новая версия теперь работает со стилями, в основном позволяя вам использовать ее в любом месте, где вы можете использовать привязку или динамический ресурс, и получать ожидаемые результаты, делая его чрезвычайно полезным.

Технически это не вопрос. Это сообщение, показывающее способ, который я нашел, чтобы легко использовать преобразователи с DynamicResource в качестве источника, но для того, чтобы следовать рекомендациям s/o, я отправляю его как пару вопросов/ответов. Поэтому ознакомьтесь с моим ответом ниже, как я это сделал. Надеюсь, поможет!

4b9b3361

Ответ 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 с полной поддержкой конвертеров, строковых форматов, обработки нулевого значения и т.д.!

Во всяком случае, это! Я действительно надеюсь, что это поможет другим разработчикам, поскольку это действительно упростило наши шаблоны управления, особенно вокруг общих границ границ и т.д.

Наслаждайтесь!