Pull to refresh

BindableConverter для WPF

Reading time4 min
Views5.4K
Проблема: WPF классная технология, но местами недоработанная. Например, вот такой код выплюнет не помню точно какой Exception, поскольку ConverterParameter не является наследником DependencyObject'a:

<...Text={Binding SourceProperty, Converter={StaticResource SomethingToSomethingElseConverter} ConverterParameter={Binding AnotherSourceProperty}} />

Собственно, это и проблема. А ниже ее решение.

Обновление от 18.01.16:
1) Если базовый класс конвертера унаследовать от Freezable, то прокси просто не нужен. В таком случае конвертер работает в точности как работал бы обычный:
<что-то.Resources>
<converters:такой-то_конвертер x:Key="такой-то_ключ" BindingParameter1="{Binding такое-то_свойство}" />
</что-то.Resources>

… пожалуйста, учтите это при чтении!


В принципе, решить проблему можно двумя путями: первое DependencyProperty.Register(..), второе — .RegisterAttached(..). Разница в том, что второй вариант и концептуально, и архитектурно ущербный. Поэтому вот так:

using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace BindableConverter
{
    [ValueConversion(typeof(object), typeof(object))]
    public class BindableConverterBase : DependencyObject, IValueConverter, IMultiValueConverter
    {
        #region BindableParameters

        #region BindableParameter1
        public object BindableParameter1
        {
            get { return GetValue(BindableParameter1Property); }
            set { SetValue(BindableParameter1Property, value); }
        }

        public static readonly DependencyProperty BindableParameter1Property = DependencyProperty.Register(
                nameof(BindableParameter1),
                typeof(object),
                typeof(BindableConverterBase),
                new PropertyMetadata(String.Empty)
                );
        #endregion

        #region BindableParameter2
        public object BindableParameter2
        {
            get { return GetValue(BindableParameter2Property); }
            set { SetValue(BindableParameter2Property, value); }
        }

        public static readonly DependencyProperty BindableParameter2Property = DependencyProperty.Register(
                nameof(BindableParameter2),
                typeof(object),
                typeof(BindableConverterBase),
                new PropertyMetadata(String.Empty)
                );
        #endregion

        #region BindableParameter3
        public object BindableParameter3
        {
            get { return GetValue(BindableParameter3Property); }
            set { SetValue(BindableParameter3Property, value); }
        }

        public static readonly DependencyProperty BindableParameter3Property = DependencyProperty.Register(
                nameof(BindableParameter3),
                typeof(object),
                typeof(BindableConverterBase),
                new PropertyMetadata(String.Empty)
                );
        #endregion

        #endregion  

        #region IValueConverter
        public virtual object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }

        public virtual object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
        #endregion

        #region IMultiValueConverter
        public virtual object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }

        public virtual object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
        #endregion
    }
}


Поскольку класс реализовал оба интерфейса, то будет работать и с обычным Binding'ом, и с MultiBinding. Собственно, осталось только унаследоваться от BindableConverter, переопределив нужный (или нужные, что редкость) методы.

В использовании в XAML есть один принципиальный момент. Наивная попытка типа:
<Window.Resources>
 <local:NameAndAgeToVladimirPutinConverter x:Key="NameAndAgeToVladimirPutin" 
                 BindableParameter1="{Binding FirstName}"
                 BindableParameter2="{Binding Age}" />
</Window.Resources>

… приведет к тому, что оба параметра будут равны дефолтному значению. Всегда — независимо от того, где именно в ресурсах вы объявите ссылку на конвертор.

Признаюсь, окончательного понимания почему так происходит, у меня нет. В общих словах смысл в том, что конвертор находится вне Logical\VisualTree UI-элементов, поэтому привязаться ему просто не к кому. Во всяком случае, именно такое объяснение я раскопал на StackOverflow. Решение проблемы выглядит вот так:

<bc:BindingProxy x:Key="BindingProxy" Data="{Binding}" />
<local:NameAndAgeToVladimirPutinConverter x:Key="NameAndAgeToVladimirPutin" 
              BindableParameter1="{Binding Source={StaticResource BindingProxy}, Path=Data.FirstName}"
              BindableParameter2="{Binding Source={StaticResource BindingProxy}, Path=Data.Age}"/>
...
<TextBlock Grid.Row="0" Text="{Binding Name, Converter={StaticResource NameAndAgeToVladimirPutin}}" />


using System.Windows;

namespace BindableConverter
{
    public class BindingProxy : Freezable
    {
        protected override Freezable CreateInstanceCore()
        {
            return new BindingProxy();
        }

        /// <summary>
        /// Binding data.
        /// </summary>
        public object Data
        {
            get { return GetValue(DataProperty); }
            set { SetValue(DataProperty, value); }
        }

        public static readonly DependencyProperty DataProperty = DependencyProperty.Register(
                nameof(Data), 
                typeof(object),
                typeof(BindingProxy), 
                new UIPropertyMetadata(null)
            );
    }
}


P.S. В планах есть прикрутить MarkupExtension, чтобы можно было вытворять что-то типа Converter={bc:BindableConverter BindingProxy={StaticResource Proxy}, Parameter1=FirstName, Parameter2=Age}. Если у кого-то есть идеи как это сделать еще красивее и лаконичнее, то пожалуйста.

Также очень актуален вопрос о том, почему все таки без BindingProxy не работает.
Tags:
Hubs:
+5
Comments8

Articles