Bindable Converter Parameter
A simple technique to achieve Bindable-ConverterParameter in WPF's XAML.
- Download BindableConverterParameter.zip (Take1-partial)
- Download BindableConverterParameterTake2.zip(Take2-partial)
- Download BindableConverterParameterTake3.zip
Introduction
Binding is one of the most powerful features introduced in WPF, it allows us to implement complex data-based UI, using minimum of declarative code (XAML), while relying on a 'built-in', out of the box mechanism that connects data to UI-controls in a direct way or through Converters.
Binding converters are by nature an encapsulated functions that receive the bound value, manipulate it in a specific manner, and return the manipulated result. On many occasions another source of information is needed for the converter in order to 'do its job', this source is called 'ConverterParameter
' and it's one of the converter's Convert
and ConvertBack
method parameters.
WPF's out-of-the-box Bindings restricts the ConverterParameter
to receive (from XAML) only Static or Resource values, while passing it a Binding expression is forbidden:
//
// this line of xaml produce the following error:
//
<TextBox Text="{Binding Path=FirstName,Converter={StaticResource TestValueConverter},ConverterParameter={Binding Path=ConcatSign}}" />
A 'Binding' cannot be set on the 'ConverterParameter' property of type 'Binding'. A 'Binding' can only be set on a DependencyProperty of a DependencyObject.
In many cases this feature (Bindable ConverterParameter
) is exactly what ones need. I.e., the ConverterParameter
value which is a Binding-Expression (rather than a static predefined value) will be evaluated each time the binding evaluation is triggered, and will passed to the Converter as the ConverterParameter
parameter.
In this article I'll show a relatively simple and straightforward technique to achieve just that. The XAML part that will implement the bound ConverterParameter
will look as close as possible to the Error producing line of code above, with the exception that it will work...
Bindable Converter Parameter Take-3
Updates
- 29/6/13
- Added feature : Element-Name Binding
- Bug Fix : Memory-Leak
- Added feature : Element-Name Binding
- Bug Fix : Memory-Leak
This is my Third attempt in achieving a robust, simple solution for this all to well known handicap.
Main Goal
Forging The Concepts described in the previous attemts into Real-World, Robust solution
Supported Features & Virtues:
- Bindable Converter Parameter/s (Also a mix of Bindings, Static-values,
Enums*)
* there are better ways to implement Enum-Based-Binding, The addition of ConverterParameter's Enum support to BcpBinding Markup-Extension class, is ment for demonstrating its ability to handle new, different, kinds of types, while applying only minor code modifications. - Multi-Binding support
- Two-Way Binding support
- Readable comprehensible XAML :
<!--THIS SAMPLE SHOWS SINGLE-BINDING WITH BINDED/MIXED ConverterParameters SYNTAX--> <TextBox > <TextBox.Text > <local:BcpBinding Path="FirstName" ConverterParameters="Binding Path=ConcatSign,Binding Path=ConcatSign2" Converter="{StaticResource TestValueConverterWithTwoObjectsParameter}" Mode="TwoWay"/> </TextBox.Text> <TextBox.Tag > <local:BcpBinding Path="FirstName" ConverterParameters="Binding Path=ConcatSign,Binding Path=ConcatSign2,[some not binded value]" Converter="{StaticResource TestValueConverterWithThreeObjectsParameter}" /> </TextBox.Tag> </TextBox>
<!--THIS SAMPLE SHOWS MULTI-BINDING WITH BINDED ConverterParameters SYNTAX--> <TextBox > <TextBox.Text > <local:BcpBinding Converter="{StaticResource MultiValueMultiConverterParameterConverter}" Mode="TwoWay" > Binding Path=FirstName Binding Path=LastName <local:BcpBinding.ConverterParameters> Binding Path=ConcatSign Binding Path=ConcatSign2 Binding Path=AllConcatSigns </local:BcpBinding.ConverterParameters> </local:BcpBinding> </TextBox.Text> </TextBox> <!--THIS SAMPLE SHOWS MULTI-BINDING WITH MIXED ConverterParameters SYNTAX--> <TextBox > <TextBox.Text > <local:BcpBinding Converter="{StaticResource MultiValueMultiMixedConverterParameterConverter}" Mode="TwoWay" ConverterParameters="Binding Path=ConcatSign,Binding Path=ConcatSign2,[some not binded value],Binding Path=AllConcatSigns"> Binding Path=FirstName Binding Path=LastName </local:BcpBinding> </TextBox.Text> </TextBox>
- Binded ConverterParameter change DOES NOT
trigger reevaluation of the Binding(including converter):
In my humble opinion, Binded ConverterParameter change Shold not trigger the binding it is a member of.
For if we had wanted this behevior we'd placed it as another Binding inside an, already existing, out-of the-box, MultiBinding.
As i see it, ConverterParameter is just an extara data the Converter needs in order to perform it's job (whether it is a static value, or a value evalueted from a binding expression)
The only scenario i know of, on which we would want a binding to trigger a 'Parent-Binding' & also come as a ConverterParameter is on a MultiValueConverter ConvertBack case. here, we are expected to extract multiple values out of a single value and might need the original values, the ConvertBack's value, is made of.
On such a case i would use asyntax like this:<TextBox > <TextBox.Text > <local:BcpBinding Converter="{StaticResource some-converter}" Mode="TwoWay" ConverterParameters="Binding Path=FirstName, Binding Path=LastName"> Binding Path=FirstName Binding Path=LastName </local:BcpBinding> </TextBox.Text> </TextBox>
- Binding to Indexers (i.e. SomeIndexer[]) support. Both in Bindings & ConverterParameter-Bindings.
Comments:
- As this solution is based on the previous ones , i've omitted explanations in this section(TAKE3). I've placed Comments inside the code instead.
- Binding syntax is weakly-typed as Bindings & ConverterParameters are passed as strings to the Markup-Extenssion.
- I have Omitted Error-Validations in the code ,as it adds extra code which obscures the relevant code that implements the idea of the solution.
#End of TAKE3
Bindable Converter Parameter Take-2
This is my second attempt in achieving a robust, simple solution for this all to well known handicap.
Main Goals:
- XAML code that will resemble the intended code as possible.
- Simple & short.
- Minimal effect(Impact) over existing code (both in xaml & overall project).
- Support for unlimited number of Bindable-ConverterParameter bindings per element.
- Support for Two-Way-Binding (for the Binding that has the Bindable ConverterParameter).
Solution Principles
- Let user write XAML in a form that is as close as possible to the desired (unavailable form). I chose this:
- Create an Outside-Mechanism that will stitch things up 'Behind the scenes' in order to achieve the desired functionality.
<TextBox Text="{Binding Path=FirstName,Converter={StaticResource
TestValueConverter},ConverterParameter='Binding Path=ConcatSign',Mode=TwoWay}" />
The solution itself:
- Write XAML as you would under normal circumstances, when ever you need to apply 'Bindable ConverterParameter' write it as shown in the above code.
- For the Element, that has in It's scope those special kind of bindings, Register for the
_Initialized
event in code-behind (for those who are MVVM fanatics i.e. discards the use of code-behind under any circumstances, or when there is no code behind using Behavior is advised) - In the _Initialized event handler do the following:
- Get all DependencyObjects under the Initialized-Element scope.
- For each DependencyObject - Get All Dependecy-Properties that has this special form of Binding syntax.
- For each DependecyProperty do the following (see comments in code):
// 1. get original binding
Binding bindingOrig = BindingOperations.GetBinding(item, dp);
//create set of attached properties to replase this ConverterParameter Binding
//give unique name by using dp's name:
DependencyProperty apBindingSource=null;
//(5.) another attached prop for two-way binding operations
DependencyProperty apIsSourceChanged =
DependencyProperty.RegisterAttached(dp.Name + "IsSourceChanged", typeof(bool),
item.GetType(), new PropertyMetadata(false));
// 1. attached-prop for the ConverterParameter-Binding
DependencyProperty apConverterParameterBindingSource =
DependencyProperty.RegisterAttached(dp.Name + "ConverterParameterBindingSource",
typeof(object), item.GetType(), new PropertyMetadata(null));
Binding bindingConverterParameterBindingSource = new Binding(sConverterParameterBindingPath);
BindingOperations.SetBinding(item, apConverterParameterBindingSource,
bindingConverterParameterBindingSource);
// 2. attached-prop to hold the Converter Object
DependencyProperty apConverter = DependencyProperty.RegisterAttached(dp.Name +
"Converter", typeof(IValueConverter), item.GetType(), new PropertyMetadata(null));
(item).SetValue(apConverter, bindingOrig.Converter);
//3. attached-prop to hold the evaluate result - will be binded to the original Binded dp
DependencyProperty apEvaluatedResult = DependencyProperty.RegisterAttached(dp.Name +
"EvaluatedResult", typeof(object), item.GetType(), new PropertyMetadata(null, (s, edp) =>
{
if (!(bool)s.GetValue(apIsSourceChanged))
{
// change didn't come from source - target got changed in two-way binding
// change source via convert back
object ret= (s.GetValue(apConverter) as IValueConverter).
ConvertBack(edp.NewValue, null, s.GetValue(apConverterParameterBindingSource), null);
s.SetValue(apBindingSource, ret);
}
}));
// 4. attached-prop to replace the source binding - bind apBindingSource
// to the original source (instead of the original dp)
apBindingSource = DependencyProperty.RegisterAttached(dp.Name +
"BindingSource", typeof(object), item.GetType(), new PropertyMetadata(null, (s, edp) =>
{
s.SetValue(apIsSourceChanged, true);
s.SetValue(apEvaluatedResult, (s.GetValue(apConverter) as IValueConverter).Convert(
edp.NewValue, null, s.GetValue(apConverterParameterBindingSource), null));
s.SetValue(apIsSourceChanged, false);
}));
Binding NewBindingToSource = new Binding(bindingOrig.Path.Path);
NewBindingToSource.Mode =bindingOrig.Mode;
// reroute source to apBindingSource
BindingOperations.SetBinding(item, apBindingSource, NewBindingToSource);
Basically what we are doing is to replace the 'special' binding with a set of uniquely named attached properties. Then reroute the original bindings route through those attached properties. The following sketch will try to clear this up(ap=AttachedProperty):
This:
TARGET <---Binding---> SOURCE using (CONVERTER-PARAMETER <---Binding---> CONVERTER-PARAMETER SOURCE) via CONVERTER
Becomes:
TARGET <---Binding---> Result_ap <---get updated-- (Souce_ap <---Binding---> SOURCE) using (ConverterParameter_ap <---Binding---> CONVERTER-PARAMETER SOURCE ) via Converter_ap
#End of TAKE2
A Note
The following parts will describe my solution for the problem at hand. As this solution is fairly simple, I've chose not to make it more complex than it should. That is why, for example, I chose to use a pre-defined set of bindings in the Multi-Binding solution rather than add an elaborated mechanism that will obscure the main ideas I'm hoping to convey. This solution is a variant of a technique I've already shown in my article 'Silverlight Multi-Binding' with some WPF and context specific modifications and tweaks.
Using the code
Known facts
- Bindings 'naturally' 'lives' inside the framework, where they re-evaluate themselves in response to the relevant
PropertyChanged
events. - Attached properties can have binding.
- Attached Properties are subset of Dependency Properties, hence they have a Property-Changed-handling built-in mechanism.
- Custom Markup Extension can mimic (emulate) other types of markup extensions known as 'Binding'.
Ingredients
For this solution we'll need the following classes:
- CustomBindingUtil.cs: This class will hold a set of attached properties that will be attached to the Framework-element we wish to have the binding. Also, it will perform the actual value-production in response to the Binding/s changes. I've divided this class into three regions: Single-Binding, Multi-Binding, Shared.
#region Single Binding Attached-Properties
public static object GetSingleBinding(DependencyObject obj)
{
return (object)obj.GetValue(SingleBindingProperty);
}
public static void SetSingleBinding(DependencyObject obj, object value)
{
obj.SetValue(SingleBindingProperty, value);
}
// Using a DependencyProperty as the backing store for SingleBinding.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty SingleBindingProperty =
DependencyProperty.RegisterAttached("SingleBinding", typeof(object),
typeof(CustomBindingUtil), new PropertyMetadata(null, SingleBindingChanged));
private static void SingleBindingChanged(DependencyObject obj,
DependencyPropertyChangedEventArgs e)
{
try
{
object convparam = obj.GetValue(ConverterParameterProperty);
object binding = obj.GetValue(SingleBindingProperty);
obj.SetValue(BResultProperty, (obj.GetValue(ConverterProperty)
as IValueConverter).Convert(binding, null, convparam, null));
}
catch (Exception ex)
{
throw;
}
}
public static object GetBResult(DependencyObject obj)
{
return (object)obj.GetValue(BResultProperty);
}
public static void SetBResult(DependencyObject obj, object value)
{
obj.SetValue(BResultProperty, value);
}
// Using a DependencyProperty as the backing store for BResult.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty BResultProperty =
DependencyProperty.RegisterAttached("BResult", typeof(object),
typeof(CustomBindingUtil), new UIPropertyMetadata(null));
#endregion
#region Multi Binding Attached-Properties
#region Predefined Bindings
public static Object GetBinding1(DependencyObject obj)
{
return (Object)obj.GetValue(Binding1Property);
}
public static void SetBinding1(DependencyObject obj, Object value)
{
obj.SetValue(Binding1Property, value);
}
// Using a DependencyProperty as the backing store for Binding1.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty Binding1Property =
DependencyProperty.RegisterAttached("Binding1", typeof(Object),
typeof(CustomBindingUtil), new PropertyMetadata(null, BindingChanged));
public static Object GetBinding2(DependencyObject obj)
{
return (Object)obj.GetValue(Binding2Property);
}
public static void SetBinding2(DependencyObject obj, Object value)
{
obj.SetValue(Binding2Property, value);
}
// Using a DependencyProperty as the backing store for Binding2.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty Binding2Property =
DependencyProperty.RegisterAttached("Binding2", typeof(Object),
typeof(CustomBindingUtil), new PropertyMetadata(null, BindingChanged));
public static Object GetBinding3(DependencyObject obj)
{
return (Object)obj.GetValue(Binding3Property);
}
public static void SetBinding3(DependencyObject obj, Object value)
{
obj.SetValue(Binding3Property, value);
}
// Using a DependencyProperty as the backing store for Binding3.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty Binding3Property =
DependencyProperty.RegisterAttached("Binding3", typeof(Object),
typeof(CustomBindingUtil), new PropertyMetadata(null, BindingChanged));
#endregion
/// <summary>
/// update result property with converted (& Weighted) all available sub-binding results
/// </summary>
/// <param name="obj"></param>
/// <param name="e"></param>
private static void BindingChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
object[] Values = new Object[] { obj.GetValue(Binding1Property),
obj.GetValue(Binding2Property), obj.GetValue(Binding3Property) };
try
{
object convparam = obj.GetValue(ConverterParameterProperty);
obj.SetValue(MBResultProperty, (obj.GetValue(MultiConverterProperty)
as IMultiValueConverter).Convert(Values, null, convparam, null));
}
catch (Exception ex)
{
throw;
}
}
public static IMultiValueConverter GetMultiConverter(DependencyObject obj)
{
return (IMultiValueConverter)obj.GetValue(MultiConverterProperty);
}
public static void SetMultiConverter(DependencyObject obj, IMultiValueConverter value)
{
obj.SetValue(MultiConverterProperty, value);
}
// Using a DependencyProperty as the backing store for MultiConverter.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty MultiConverterProperty =
DependencyProperty.RegisterAttached("MultiConverter",
typeof(IMultiValueConverter), typeof(CustomBindingUtil), new UIPropertyMetadata(null));
public static Object GetMBResult(DependencyObject obj)
{
return (Object)obj.GetValue(MBResultProperty);
}
public static void SetMBResult(DependencyObject obj, Object value)
{
obj.SetValue(MBResultProperty, value);
}
// Using a DependencyProperty as the backing store for MBResult.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty MBResultProperty =
DependencyProperty.RegisterAttached("MBResult", typeof(Object),
typeof(CustomBindingUtil), new PropertyMetadata(null));
#endregion
#region Shared Attached-Properties
public static IValueConverter GetConverter(DependencyObject obj)
{
return (IValueConverter)obj.GetValue(ConverterProperty);
}
public static void SetConverter(DependencyObject obj, IValueConverter value)
{
obj.SetValue(ConverterProperty, value);
}
// Using a DependencyProperty as the backing store for Converter.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty ConverterProperty =
DependencyProperty.RegisterAttached("Converter",
typeof(IValueConverter), typeof(CustomBindingUtil), new UIPropertyMetadata(null));
public static object GetConverterParameter(DependencyObject obj)
{
return (object)obj.GetValue(ConverterParameterProperty);
}
public static void SetConverterParameter(DependencyObject obj, object value)
{
obj.SetValue(ConverterParameterProperty, value);
}
// Using a DependencyProperty as the backing store for ConverterParameter.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty ConverterParameterProperty =
DependencyProperty.RegisterAttached("ConverterParameter",
typeof(object), typeof(CustomBindingUtil), new PropertyMetadata(null));
#endregion
{Binding...}
Markup-Extension,
only with the added value of the Bindable ConverterParameter feature. For doing that it has three matching properties : Binding (of type Binding
),
Converter (of type IValueConverter
), and ConverterParameter (of type Binding
) in the Markup-Extension's ProvideValue
override method:Bindings (and property) are set to the element's matching attached properties, and the return value is set to a new
Binding's (referencing the element's CustomBindingUtil.BResultProperty
Attached property) - the ProvideValue
method.
public Binding Binding { get; set; }
public IValueConverter Converter { get; set; }
public Binding ConverterParameter { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider)
{
try
{
IProvideValueTarget pvt = serviceProvider.GetService(
typeof(IProvideValueTarget)) as IProvideValueTarget;
DependencyObject TargetObject = pvt.TargetObject as DependencyObject;
BindingOperations.SetBinding(TargetObject,
CustomBindingUtil.SingleBindingProperty, Binding);
BindingOperations.SetBinding(TargetObject,
CustomBindingUtil.ConverterParameterProperty, ConverterParameter);
TargetObject.SetValue(CustomBindingUtil.ConverterProperty, this.Converter);
Binding b = new Binding();
b.Source = TargetObject;
b.Path = new PropertyPath(CustomBindingUtil.BResultProperty);
return b.ProvideValue(serviceProvider);
}
catch (Exception)
{
throw;
}
}
MultiBinding
. A better (more elegant) way to handle multiple bindings is advice- see above note. The mechanism (for single binding)
Starting in the XAML:
The code is fairly close to the known binding syntax:
<TextBox >
<TextBox.Text >
<local:MyBindedParameterBinding Converter="{StaticResource TestValueConverter}"
Binding="{Binding Path=FirstName}"
ConverterParameter="{Binding Path=ConcatSign}"/>
</TextBox.Text>
</TextBox>
When the FirstName
property get changed, the element's Binding
AttachedProperty gets changed (as it is bound to FirstName
). Which triggers the SingleBindingChanged
method (inside CustomBindingUtil.cs), there BResultProperty
(another Attached property) is set to the evaluated value (the result of the value, passed into the Converter's Convert
method along with a currently evaluated ConverterParameter
parameter).
private static void SingleBindingChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
try
{
object convparam = obj.GetValue(ConverterParameterProperty);
object binding = obj.GetValue(SingleBindingProperty);
obj.SetValue(BResultProperty, (obj.GetValue(ConverterProperty)
as IValueConverter).Convert(binding, null, convparam, null));
}
catch (Exception ex)
{
throw;
}
}
The BResultProperty
is what our custom Markup-Extension is returning via binding.
public override object ProvideValue(IServiceProvider serviceProvider)
...
Binding b = new Binding();
b.Source = TargetObject;
b.Path = new PropertyPath(CustomBindingUtil.BResultProperty);
return b.ProvideValue(serviceProvider);
}
catch (Exception)
{
throw;
}
}
In Conclusion
The technique described here can be utilized by any one who faces the need for a Bindable ConverterParameter. Just add CustomBindingUtil.cs and MyBindedParameterBinding.cs or/and MyBindedParameterMultiBinding.cs to your project, and you are done. The example shown here is not intended to be a robust, production-oriented code-product as it has many loose ends (such as when trying to use more than one Bindable ConverterParameter), nevertheless it can by broadened to become one.