Проблема: WPF классная технология, но местами недоработанная. Например, вот такой код выплюнет не помню точно какой Exception, поскольку ConverterParameter не является наследником DependencyObject'a:
Собственно, это и проблема. А ниже ее решение.
Обновление от 18.01.16:
1) Если базовый класс конвертера унаследовать от Freezable, то прокси просто не нужен. В таком случае конвертер работает в точности как работал бы обычный:
… пожалуйста, учтите это при чтении!
В принципе, решить проблему можно двумя путями: первое DependencyProperty.Register(..), второе — .RegisterAttached(..). Разница в том, что второй вариант и концептуально, и архитектурно ущербный. Поэтому вот так:
Поскольку класс реализовал оба интерфейса, то будет работать и с обычным Binding'ом, и с MultiBinding. Собственно, осталось только унаследоваться от BindableConverter, переопределив нужный (или нужные, что редкость) методы.
В использовании в XAML есть один принципиальный момент. Наивная попытка типа:
… приведет к тому, что оба параметра будут равны дефолтному значению. Всегда — независимо от того, где именно в ресурсах вы объявите ссылку на конвертор.
Признаюсь, окончательного понимания почему так происходит, у меня нет. В общих словах смысл в том, что конвертор находится вне Logical\VisualTree UI-элементов, поэтому привязаться ему просто не к кому. Во всяком случае, именно такое объяснение я раскопал на StackOverflow. Решение проблемы выглядит вот так:
P.S. В планах есть прикрутить MarkupExtension, чтобы можно было вытворять что-то типа Converter={bc:BindableConverter BindingProxy={StaticResource Proxy}, Parameter1=FirstName, Parameter2=Age}. Если у кого-то есть идеи как это сделать еще красивее и лаконичнее, то пожалуйста.
Также очень актуален вопрос о том, почему все таки без BindingProxy не работает.
<...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 не работает.
IL_Agent
Да ну? Пример с эксепшном не приведёте?
52hertz
ну вот прям чесслово! сами попробуйте прибиндить ConverterParameter куда-нить!
IL_Agent
Да, работать не будет, вы правы. Но вы как-то скомкано описали причину. Binding.ConverterParameter — не DependencyProperty, поэтому ему нельзя присвоить Binding, вот и всё.
Смысл ответа, который вы нашли, близок к истине. Создаваемый объект конвертера не наследует DataContext окна. Да и как ему это делать? Мало того, что он не визуальный элемент, так у него даже свойства DataContext нет. :) Где ему искать FirstName и Age?
Поясните, а зачем этот велосипед? Почему бы просто не использовать MultiBinding / IMultiValueConverter?
Hydro
По этой же причине хотел вступить в полемику с автором поста, но зашел в его профиль и посмотрел предыдущие записи в блоге… После этого, желание полемизировать отпало…
52hertz
Ну да, не депендеси. Я по-моему про это и написал.
отпадает. Да и места занимает гораздо больше (как на мой вкус — одна из самых неприятных проблем с XAML'ом — количество текста).Я все-таки не считаю это велосипедом. Не знаю, следите ли вы за интервью и выступлениями команды, которая занимается wpf, на 9channel, но и они признают, что wpf нужно развивать также в сторону все более и более легкого, удобного, интуитивно понятного использования конечным пользователем вроде нас с вами. Лично мне ужасно не нравится печатать многа букаф, поэтому вариант с
Кстати, если конвертор унаследовать напрямую от Freezable, то прокси не нужен. И тогда объявление конвертора — в точности такое же, как обычного — где-нибудь в ресурсах, только с использованием лаконичного markup-ex {Binding Name}. Собственно, для меня смысл в этом.
Nanako
А не проще запилить именованый инстанс вспомогательного класса и к нему как к ресурсу привязаться. Конвертер же тоже ресурс, он DependencyObject из чисто архитектурных соображений не может быть. Т.е. можно нагородить велосипедов, но зачем?!
52hertz
В каком смысле — не проще? Не проще в смысле экономии времени? Да, в этом (и только в этом!) смысле — действительно проще. Но ИМХО это засоряет код, что уже само по себе смертный грех. И почему это конвертер не может быть объектом зависимости? Кто именно запрещает? По-моему, это очень логично: конвертору для нормальной работы в условиях реальных задач необходимы вспомогательные параметры; по сути, результат конвертации ЗАВИСИТ от этих параметров, что можно перевести в «конвертор (конкретный экземпляр соотв. класса) зависит от этих параметров». Что еще нужно?
Nanako
A IMultiValueConverter тогда вам чем не угодил? А, ну да, букв много.