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
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 :
-->
<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>
-->
<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>
-->
<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:
<TextBox Text="{Binding Path=FirstName,Converter={StaticResource
TestValueConverter},ConverterParameter='Binding Path=ConcatSign',Mode=TwoWay}" />
- Create an Outside-Mechanism that will stitch things up 'Behind the scenes' in order to achieve the desired functionality.
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):
Binding bindingOrig = BindingOperations.GetBinding(item, dp);
DependencyProperty apBindingSource=null;
DependencyProperty apIsSourceChanged =
DependencyProperty.RegisterAttached(dp.Name + "IsSourceChanged", typeof(bool),
item.GetType(), new PropertyMetadata(false));
DependencyProperty apConverterParameterBindingSource =
DependencyProperty.RegisterAttached(dp.Name + "ConverterParameterBindingSource",
typeof(object), item.GetType(), new PropertyMetadata(null));
Binding bindingConverterParameterBindingSource = new Binding(sConverterParameterBindingPath);
BindingOperations.SetBinding(item, apConverterParameterBindingSource,
bindingConverterParameterBindingSource);
DependencyProperty apConverter = DependencyProperty.RegisterAttached(dp.Name +
"Converter", typeof(IValueConverter), item.GetType(), new PropertyMetadata(null));
(item).SetValue(apConverter, bindingOrig.Converter);
DependencyProperty apEvaluatedResult = DependencyProperty.RegisterAttached(dp.Name +
"EvaluatedResult", typeof(object), item.GetType(), new PropertyMetadata(null, (s, edp) =>
{
if (!(bool)s.GetValue(apIsSourceChanged))
{
object ret= (s.GetValue(apConverter) as IValueConverter).
ConvertBack(edp.NewValue, null, s.GetValue(apConverterParameterBindingSource), null);
s.SetValue(apBindingSource, ret);
}
}));
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;
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);
}
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);
}
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);
}
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);
}
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);
}
public static readonly DependencyProperty Binding3Property =
DependencyProperty.RegisterAttached("Binding3", typeof(Object),
typeof(CustomBindingUtil), new PropertyMetadata(null, BindingChanged));
#endregion
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);
}
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);
}
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);
}
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);
}
public static readonly DependencyProperty ConverterParameterProperty =
DependencyProperty.RegisterAttached("ConverterParameter",
typeof(object), typeof(CustomBindingUtil), new PropertyMetadata(null));
#endregion
MyBindedParameterBinding.cs - This is a custom Markup-Extension class. Its main goal is to emulate the known {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;
}
}
MyBindedParameterMultiBinding.cs - the same principles as in MyBindedParameterBinding.cs with additional tweaks in order
to support 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.