Introduction
This article presents a MultiBinding
implementation for Silverlight 5, enabling aggregating values from several sources to one target dependency property. In contrast to WPF, Silverlight only supports single-value Binding out-of-the-box, but thanks to the support for custom markup extensions introduced in Silverlight 5, it was possible to write a MultiBinding
implementation with similar syntax and functionality as the WPF version of MultiBinding
.
This markup extension MultiBinding
supports all types of source bindings including data context sensitive ones and bindings with named elements and RelativeSource
. It also supports two-way binding, i.e., converting back from a single target value to multiple source values. It can also be applied to several elements using styling. In some aspects, this Silverlight version extends the WPF MultiBinding
:
- Source bindings can be declared using XAML markup attribute-syntax (
{z:MultiBinding}
) as a complement to specifying the source bindings in a collection using the element syntax (<z:MultiBinding>
).
- The
MultiBinding
properties Converter
, ConverterParameter
and StringFormat
can be bound dynamically to arbitrary sources using Bindings
.
- Sources are not restricted to
Bindings
. String
constants, XAML objects, StaticResource
and other markup extensions can be used as individual sources.
- In two-way binding, the normal Silverlight validation mechanism can be used when exceptions are thrown from the user-provided
IMultiValueConverter.ConvertBack
implementation.
- Converters implementing the single-value
IValueConverter
can also be used with this MultiBinding
, making it useful for single source bindings with Converter
, ConverterParameter
or StringFormat
changing during run-time.
Background
Unlike an ordinary Binding, MultiBinding
allows bindings of more than one source to a single dependency property. One common usage is to present a customized text that includes values from several sources. For such scenarios, MultiBinding
offers a StringFormat
property to define the format with placeholders for the source values to be inserted. But the usage is not restricted to text. A custom IMultiValueConverter
can be used to specify how the source values are to be combined.
In WPF, a MultiBinding
must be defined in XAML element syntax like this:
<TextBlock>
<TextBlock.Text>
<MultiBinding StringFormat="{}{0} {1}" >
<Binding Path="FirstName" />
<Binding Path="LastName" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
For Silverlight 3 and 4, Colin Eberhardt has presented a solution for MultiBinding
based on an attached property (see this link and this link). In comparison, this Silverlight 5 solution presented here supports markup extension syntax, bindable Converter
, ConverterParameter
and StringFormat
properties and fully supports source bindings with ElementName
and RelativeSources
. Moreover, bindable properties are not restricted to those in the System.Windows.Control
namespace. On the other hand, it is not possible to directly use the MultiBinding
solution presented below in WPF, mainly because the MultiBinding
uses the Silverlight-specific IMarkupValue<T>
interface.
At the time of writing this article, ntg123 published another Silverlight MultiBinding
solution. See the article Silverlight Multi-Binding for more information.
When using the Model-View-ViewModel pattern (MVVM), MultiBinding
is often not essential. Aggregation of several (model) sources can be performed in a view-model property. Care must however be taken to ensure that change notification is done for the aggregated property whenever one of the source values are changed. Although MVVM also results in more unit testable code, sometimes it is not desired or necessary to use this pattern, if the same result can be achieved simpler by bindings in the view or to the model directly. I encourage you to think about alternative solutions for the MultiBinding
use case examples shown below, for instance using the MVVM pattern.
Using the Code
Simple Model Binding with Text Formatting
If we have a model or view model object with properties for FirstName
and LastName
as the data context, we can use the MultiBinding
to present the full name in this way:
<TextBlock Text="{z:MultiBinding Source1={Binding FirstName},
Source2={Binding LastName}, StringFormat='%1, %0' }" />
Whenever FirstName
or LastName
is modified, the text will be updated provided that the model object supports change notification. MultiBinding
supports up to 5 sources to be specified as attributes, so as long as the number of sources is not greater, simple XAML attribute syntax can be used. When the number of sources grows larger or some of the properties cannot be specified in attribute syntax, the XAML element syntax can be used instead as shown in the next example where we use MultiBinding
in a list box data template to show full names of Persons
. The user can select the name format in a combo box.
<ComboBox Name="cboNameFormat" Width="180" SelectedValuePath="Tag" >
<ComboBoxItem Tag="%0 %1" IsSelected="True">First and last name</ComboBoxItem>
<ComboBoxItem Tag="%1, %0">Last name, first name</ComboBoxItem>
</ComboBox>
<ListBox ItemsSource="{Binding Persons}" >
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock>
<TextBlock.Text>
<local:MultiBinding StringFormat=
"{Binding SelectedValue, ElementName=cboNameFormat}" >
<local:BindingCollection>
<Binding Path="FirstName" />
<Binding Path="LastName" />
</local:BindingCollection>
</local:MultiBinding>
</TextBlock.Text>
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
As demonstrated above, this MultiBinding
supports specifying the value placeholders in StringFormat
with %n
instead of the ordinary {n}
syntax, since the latter requires a lot of escaping when used in XAML. Another important difference compared to the WPF version is that StringFormat
can be specified as a binding as well. In the snippet above, we bind StringFormat
directly to the combobox
selected value. Changing selection will affect the formatting of all names in the listbox
, as shown in the screenshot at the top of the article.
In contrast to the WPF version StringFormat
is always applied regardless of the destination property type. The original WPF MultiBinding
only applies StringFormat
when the target property is of String
type, which means that it is not useful directly in a ContentControl
derived content.
Using a Custom IMultiValueConverter
Besides simple string
formatting, the Silverlight MultiBinding
allows you to provide a custom converter which is responsible for converting the source values to one single value to be used. The custom converter must implement the IMultiValueConverter
interface, identical to the WPF version.
In the example below, we use a MultiBinding
to enable a button when two check boxes have been selected.
<CheckBox Name="chkLicenceAccepted" >I have accepted the Licence</CheckBox>
<CheckBox Name="chkConditionsAccepted" >I have accepted the Conditions</CheckBox>
<Button Content="Continue" >
<Button.IsEnabled>
<local:MultiBinding
Source1="{Binding IsChecked, ElementName=chkLicenceAccepted}"
Source2="{Binding IsChecked, ElementName=chkConditionsAccepted}"
Converter="{local:MinNumberOfSetConverter}"
ConverterParameter="2"
/>
</ </Button.IsEnabled>
</Button>
As demonstrated above, the MultiBinding
supports source bindings with ElementName
s. RelativeSource
is also supported allowing reference to target element by using Self
or an ancestor in the visual tree by using AncestorType
(new in Silverlight 5).
The implementation of berOfSetConverter
is shown below. What we need above is just a logical AND
converter, but to be useful in many situations I wrote it in a more general fashion supporting specifying the number of set (“true
”) source values required to return true
.
public classpublic class MinNumberOfSetConverter : MarkupExtension, IMultiValueConverter
{
public object Convert(object[] values, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
int minNumberOfSet;
if( !int.TryParse(parameter as String, out minNumberOfSet) )
minNumberOfSet = values.Length;
int numberOfSet = 0;
for (int i = 0; i < values.Length; i++)
{
bool? boolValue = values[i] as bool?;
if (boolValue.GetValueOrDefault()) numberOfSet++;
if (numberOfSet >= minNumberOfSet) return true;
}
return numberOfSet >= minNumberOfSet;
}
public object[] ConvertBack(object value, Type[] targetTypes,
object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
public override Object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
}
To support convenient markup extension syntax, the converter derives from MarkupExtension
, but this is not a requirement on the converter. To reuse the same converter in several locations, it can instead be defined as a resource and referred to using StaticResource
Conversion Back from Target to Source Values
This Silverlight MultiBinding
also supports two-way binding with conversion from a single value to multiple values through the Converter
’s ConvertBack
method, similar to the WPF counterpart. In the example below, we present a length together with the unit in a text allowing the user to modify both in the same field. When the user modifies the text, the custom converter splits the quantity and the unit to its original fields:
<TextBlock Text="Length: " />
<TextBox Width="100" BindingValidationError="TextBox_BindingValidationError" >
<TextBox.Text>
<local:MultiBinding Mode="TwoWay"
NotifyOnValidationError="True"
ValidatesOnExceptions="true"
Converter="{local:LengthConverter}"
Source1="{Binding Length, Mode=TwoWay,
ValidatesOnExceptions=True}"
Source2="{Binding LengthUnit, Mode=TwoWay}" />
</TextBox.Text>
</TextBox>
To enable conversion back, Mode
must be set to TwoWay
on the MultiBinding
and on the source bindings. The converter must also implement the IMultiValueConverter.ConvertBack
method. In the example above, LengthConverter
’s implementation looks like this:
public object[] ConvertBack(object value, Type[] targetTypes,
object parameter, System.Globalization.CultureInfo culture)
{
string lengthWithUnit = value as string;
if (lengthWithUnit != null)
{
object[] result = new object[2];
string[] parts = lengthWithUnit.Split(new Char[] { ' ' },
StringSplitOptions.RemoveEmptyEntries);
result[0] = Double.Parse(parts[0].ToString());
if (parts.Length > 1)
{
result[1] = parts[1].Trim();
}
return result;
}
return null;
}
The ConvertBack
is expected to return a Object[]
array with values for each source. The input parameter targetType
indicates the types (based on the current values). If ConvertBack
returns null
, no source will be updated. If an item in the result array is set to DependencyProperty.UnsetValue
, the corresponding source will not be updated. In case the returned array is shorter than the number of sources, only the first sources will be updated.
In case an exception is thrown from the ConvertBack
method, it will cause a validation error to be set on the target element, but only when the ValidatesOnExceptions
property is set to true
. Just like ordinary Binding
s, you can also set the NotifyOnValidationError
property to true
to raise the bubbling BindingValidBindingValidationError
event when the validation error state is changed. To get validation errors when individual sources are set, you can set the ValidatesOnExceptions
or other validation related properties on the individual source bindings as well.
How It Works
To use the MultiBinding
, you do not have to read this section, but if you are interested in the implementation details and the challenges I met when implementing this, please read on.
A Custom Binding Markup Extension
When a markup extension is applied to a property, its ProvideValue
method is called. Since BindingBase
and Binding
have sealed ProvideValue
methods, it is not possible to extend the binding mechanism by simple inheritance. Instead, we can create a new MarkupExtension
derived class and let its ProvideValue
return the result from an inner, programmatically created Binding
instance. For most target properties, Binding
’s ProvideValue
returns a BindingExpression
, but for style setter values, it returns the Binding
itself.
For MultiBinding
a new inner Binding
instance is created each time the MultiBinding
is applied, i.e., ProvideValue
is called. The source of this binding is the SourceValues
property of a MultiBindingExpression
instance. Just like the BindingExpression
, one MultiBindingExpression
is created for each binding target instance:
public class MultiBinding : DependencyObject, IMarkupExtension<Object>
{
...
public object ProvideValue(IServiceProvider serviceProvider)
{
IProvideValueTarget pvt = serviceProvider.GetService
(typeof(IProvideValueTarget)) as IProvideValueTarget;
DependencyObject target = pvt.TargetObject as DependencyObject;
Binding resultBinding = ApplyToTarget(target);
return resultBinding.ProvideValue(serviceProvider);
}
private Binding ApplyToTarget(DependencyObject target)
{
Seal();
MultiBindingExpression newExpression = new MultiBindingExpression(target, this);
PropertyPath path = new PropertyPath("SourceValues");
Binding resultBinding = new Binding();
resultBinding.Path = path;
resultBinding.Source = newExpression;
resultBinding.Converter = newExpression;
resultBinding.Mode = Mode;
resultBinding.UpdateSourceTrigger = UpdateSourceTrigger;
resultBinding.TargetNullValue = TargetNullValue;
resultBinding.ValidatesOnExceptions = ValidatesOnExceptions;
resultBinding.NotifyOnValidationError = NotifyOnValidationError;
resultBinding.ConverterCulture = ConverterCulture;
resultBinding.FallbackValue = FallbackValue;
return resultBinding;
}
}
The MultiBindingExpression.SourceValues
property holds the unconverted source values from the participating bindings. The final aggregation of the source values to the final target value involves invoking any user-provided IMultiValueConverter
implementation and doing formatting according to the StringFormat
setting. This is performed in the MultiBindingExpression.Convert
method, which is an implementation of the standard single-value IValueConverter
interface. As shown in the code-snippet above, the MultiBindingExpression newExpression
is set as the Converter
on the inner binding, not just the Source
, to achieve this setup. In the first implementation, the aggregation was done before setting the SourceValues
property, but to be able to use the binding culture and target type awareness, I found it better to let MultiBindingExpression
implement the IValueConverter
and use that as a converter for the source.
Hidden Attached Properties to Support Bindings Relative to Target
Another challenge was to support all type of source bindings relative to the target, including to the data context, named elements and RelativeSource
. In the first implementation, these bindings were transferred to and managed by the MultiBindingExpression
instance, one for each time the MultiBinding
was applied. MultiBindingExpression
then derived from FrameworkElement
to support binding and DataContext
. By binding the DataContext
of the MultiBindingExpression
to the DataContext
of the target, it was possible to support binding
relative to the target’s data context. This is similar to the approach described as "virtual trees" by Josh Smith. However, supporting ElementName
and RelativeSource
is much harder with this approach since the MultiBindingExpression
is not a true part of the visual tree. Colin Ebenhardt has demonstrated a way to support ElementName
, but I chose another solution based on attached properties, attached to the MultiBinding
target object. These attached data properties are owned by the MultiBindingExpression
and set when a MultiBindingExpression
instance is created and applied to a target element. All source bindings from the MultiBinding
are then reapplied to the attached data properties, placing them in the right context to use bindings relative to DataContext
, named elements and RelativeSource
. If the XAML code was saved after the MultiBinding
has been applied, we would see the data properties as shown in the snippet below, where D0
and D1
are the name of the data properties, and MBE is an abbreviation for MultiBindingExpression
:
<TextBlock Text="{z:MultiBinding Source1={Binding FirstName}
Source2={Binding LastName} StringFormat='%0 %1'}"
MBE.MultiBindingExpression="..." MBE.D0="{Binding FirstName}"
MBE.D1="{Binding LastName}" />
To support any number of MultiBinding
s to the same target and any number of source bindings on a single MultiBinding
, the attached data properties are created dynamically as needed. They are hidden from the designer since they do not have any getters or setters. To keep track of the mapping between the attached data properties and the multibinding
an additional hidden MultiBindingExpressions
attached property is set on target. This is set to a list of MultiBindingExpressions
applied to the target element. Each MultiBindingExpression
contains private
member fields to keep track of what data property corresponds to which source property. Data properties are also used to store any binding to Converter
, ConverterParameter
and StringFormat
, allowing them to be specified relative to the target element too. Whenever the value of a source binding changes, a common property change callback method registered for all data properties is invoked. This callback method will initiate a request to update the SourceProperty
. This update will happen asynchronously by posting a request to the targets dispatcher, to allow several bindings to change values before aggregating the result, for instance when the data context changes.
A DependencyObject Derived Markup Extension to Support Binding
To support bindings to be specified for the MultiBinding
markup extension properties, MultiBinding
derives from DependencyObject
. Interestingly, this is supported in Silverlight, but not in WPF, because in Silverlight markup extension can simply implement the IMarkupExtension<T>
interface, whereas they must derive from the MarkupExtension
, derived directly from Object, in WPF. Any binding from the MultiBinding
instance is reapplied to an attached data property on the target when the MultiBinding
is applied to the target. Bindings are detected using DependencyObject.ReadLocalValue
which will return a BindingExpression
when a binding exists for the source markup property. From this BindingExpression
, we can get the ParentBinding
and rebind it to an attached data property using BindingOperation.SetBinding
:
DependencyProperty destProperty = GetDataProperty(dataPropertyIndex);
BindingExpression bindingExpression = localValue as BindingExpression;
if (bindingExpression != null)
{
Binding propertyBinding = bindingExpression.ParentBinding;
BindingOperations.SetBinding(Target, destProperty, propertyBinding);
}
else
{
Target.SetValue(destProperty, localValue);
}
Support for MultiBindings in Styles
Yet another challenge was to support specifying MultiBinding
s as values in styles just like ordinary bindings can be used in styles to be reused in multiple places like this:
<Style x:Key="myStyle1" TargetType="TextBox">
<Setter Property="Text"
Value="{z:MultiBinding Mode=TwoWay, Source1={Binding Title, Mode=TwoWay},
Converter={StaticResource stringFormatConverter}}" />
<Setter Property="Tag"
Value="{z:MultiBinding Source1={Binding Title},
Converter={StaticResource stringFormatConverter}}" />
</Style>
Without special treatment for style setter, we would apply the attached data properties on the style Setter and create an inner binding to a single MultiBindingExpression
that would be shared between all target elements using the style, resulting in unexpected behavior. What we want is to create a new MultiBindingExpression
for each element the style is applied to. To achieve this, we have to detect when a MultiBinding
is applied to a setter value property:
private static readonly PropertyInfo SetterValueProperty =
typeof(Setter).GetProperty("Value");
public object ProvideValue(IServiceProvider serviceProvider)
{
IProvideValueTarget pvt = serviceProvider.GetService
(typeof(IProvideValueTarget)) as IProvideValueTarget;
if (pvt.TargetObject is Setter && pvt.TargetProperty == SetterValueProperty)
{
Setter setter = (Setter)pvt.TargetObject;
ApplyToStyle(setter);
return this;
}
…
}
private void ApplyToStyle(Setter setter)
{
m_styleProperty = setter.Property;
setter.Property = styleMultBindingProperties[currentStyleMultiBindingIndex];
currentStyleMultiBindingIndex = (currentStyleMultiBindingIndex + 1)
% MaxStyleMultiBindingsCount;
Seal();
}
What we are doing in ApplyToStyle
above is to actually replace the Setter
's original Property
property with an internal attached property. The original setter property is stored in m_styleProperty
. When the style is applied and the attached property then is set, a registered property change callback for the attached property applies the multibinding to the property originally set in the style setter:
private static void OnStyleMultiBindingChanged
(DependencyObject d, DependencyPropertyChangedEventArgs args)
{
MultiBinding mb = (MultiBinding)args.NewValue;
if (mb != null)
{
object existingValue = d.ReadLocalValue(mb.m_styleProperty);
if (existingValue == DependencyProperty.UnsetValue)
{
Binding resultBinding = mb.ApplyToTarget(d);
BindingOperations.SetBinding(d, mb.m_styleProperty, resultBinding);
}
}
}
To support several MultiBinding
s for different properties in the same style, we are required to use different style multibinding attached properties for each of them. That is why we have an array of properties (created in the static
class constructor, not shown above). To avoid having to create a new attached property for each setter, we reuse them in a round-robin fashion. As a consequence, there is a maximum number of setters with MultiBinding
s in the same Style
, defined by the constant MaxStyleMultiBindingsCount
, currently set to 10
. It is very unlikely that more MultiBinding
s are applied in the same style, but this setting can be increased if necessary.
History
- November 16, 2011
- Initial version developed and tested with Silverlight 5 RC in Visual Studio 2010
- May 8, 2014
- Version 1.1 with upgrade to VS2013 and fixes mainly to avoid design-time errors