Introduction
WPF Binding has a nice feature. Except for
property path it allows to specify parameters in PathProperties
that will be passed to Item[]
indexer during binding. Unfortunatly, Binding
extension doesn't allow
to specify these path parameters in XAML, forcing us to use parameters hard-coded in
Path
property of extension, like:{Binding Path=property1.property2[10]}
.
Would it be nice to allow GUI designer to
specify such parameters in XAML? Consider something like this:
{Binding Property1.Property2[(0)], {Binding Path=Index}}
. In this
expression, Property2.Item[]
indexer should receive a value of Index
property of the current data context. Having this feature could reduce the view-model
component complexity allowing XAML designer to use parts of data-model as
view-model.
Background
WPF binding feature allows business logic and user interface to be
loosely coupled. It is great when GUI designer can use XAML to develop user
interface while programmer develops business logic components. In a modern
MVVM paradigm, designer and programmer both agree on view-model component's
content that extends business logic (data model) with capabilities needed for XAML to be properly
binded.
Everybody knows that programmers are lasy, so it could be a
headache for them to support two models (M and VM) simultanously in synchronized
state. Giving XAML designer extended binding capabilities could reduce
view-model complexity. One of the proposed features is a posibility to specify
parameters in binding path like described above. Here is a small example.
Using the code
Consider the following data-model and view-model:
public class Sensor
{
public string Name { get; set; }
}
public partial class MainWindow : Window
{
ObservableCollection<Sensor> _sensors = new ObservableCollection<Sensor>();
public MainWindow()
{
InitializeComponent();
_sensors.Add(new Sensor() { Name = "Sensor1" });
_sensors.Add(new Sensor() { Name = "Sensor2" });
_sensors.Add(new Sensor() { Name = "Sensor3" });
_sensors.Add(new Sensor() { Name = "Sensor4" });
}
public IList<Sensor> Sensors
{
get { return _sensors; }
}
public int this[Sensor sensor]
{
get { return _sensors.IndexOf(sensor); }
}
}
Now we want to bind the list of sensors to ListBox in XAML and show index of each sensor in collection.
Take a look on the following XAML:
<Window x:Class="Sources.MainWindow" x:Name="_window"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:emg="clr-namespace:Emightgen"
Title="Binding using custom parameters" Height="350" Width="525">
<Grid>
<ListBox
ItemsSource="{Binding ElementName=_window, Path=Sensors}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center">Name:</TextBlock>
<TextBlock VerticalAlignment="Center" Margin="5" Text="{Binding Name}" />
<TextBlock VerticalAlignment="Center">Index:</TextBlock>
<TextBlock VerticalAlignment="Center" Margin="5" Text="{emg:Binding '[(0)]', {Binding}, ElementName=_window}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
As you can see in the picture above, the list box shows index of each sensor,
dynamically evaluating parameter when template data context is changed set.
Implementation
It is very hard (almost impossible) to extend WPF controls. Nearly all
classes are sealed or internal so it just impossible to inherit them. It is
also true for Binding
extension class that should be extended for
our purposes. There are two reasons we need to inherit from Binding
.
The first one, we should allow to specify parameters in XAML and the second one to evaluate
(bind)
them when data context is changed or template is reapplied. Even if we will provide
our own markup extension to create PropertyPath
that contains parameters,
they will not be evaluated during the binding process and will be passed as instance of Binding
to indexer instead of actual value.
Fortunatly (after .NET debugging and source code reviews), I found a solution we can use. We can write our own markup extension
that emulates WPF binding and uses MultiBinding
internally to
provide a resulted value. Usage of MultiBinding
of WPF solves the
following four issues:
- Parameters specified by user will be automatically evaluated during the binding.
- Our extension can be used in
DataTrigger
where custom markup extensions are not allowed.
- Support for two way bindings.
- Receive notifications when data context is changed or template is applied.
Take a look on simplified version of our markup extension. It contains properties
that simulate WPF binding and constructor arguments to allow user to specify
parameters. This markup extension implements IMultiValueConverter
interface
that is used in internal MultiBinding
to provide a final value and IValueConverter
to remember each evaluated parameter.
public class BindingExtension : MarkupExtension, IMultiValueConverter, IValueConverter, INotifyPropertyChanged
{
string _path = null;
string _elementName = null;
object _source = null;
Collection<object> _parameters = null;
BindingMode _mode = BindingMode.Default;
public BindingExtension(string path, object arg1, object arg2)
{
_path = path;
_parameters = new Collection<object>();
_parameters.Add(arg1);
_parameters.Add(arg2);
}
[DefaultValue(null), ConstructorArgument("arg1"), EditorBrowsable(EditorBrowsableState.Never)]
public object Arg1
{
get { return _parameters[0]; }
set { _parameters[0] = value; }
}
}
Internal MultiBinding
contains binding for every parameter specified by user, special binding to
get data context and special binding to get target dependency object. These two
are needed in DataTrigger
that is DependencyObject
also but uses it's own logic to
evaluate supplied binding expression. Another special binding will be used to update target property value from the code. Our extension will be used as
converter and will provide the final value as conversion result.
public override object ProvideValue(IServiceProvider serviceProvider)
{
if (serviceProvider == null)
{
return this;
}
IProvideValueTarget provideValueTarget = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
if (provideValueTarget == null)
{
return this;
}
_targetObject = provideValueTarget.TargetObject as DependencyObject;
if (_targetObject == null)
{
return this;
}
_targetProperty = provideValueTarget.TargetProperty as DependencyProperty;
MultiBinding mbinding = new MultiBinding();
mbinding.Mode = _mode;
mbinding.Converter = this;
Binding binding1 = new Binding();
binding1.Mode = BindingMode.OneWay;
mbinding.Bindings.Add(binding1);
Binding binding2 = new Binding();
binding2.Mode = BindingMode.OneWay;
binding2.RelativeSource = new RelativeSource(RelativeSourceMode.Self);
mbinding.Bindings.Add(binding2);
Binding binding3 = new Binding();
binding3.Mode = BindingMode.OneWay;
binding3.Source = this;
binding3.Path = new PropertyPath("EffectiveValueChanged");
mbinding.Bindings.Add(binding3);
_evaluatedParameters = new Collection<object>();
for (int i = 0; i < _parameters.Count; i++)
{
object pvalue = _parameters[i];
if (pvalue is Binding)
{
Binding pbinding = pvalue as Binding;
if (!(pbinding.ConverterParameter is ParameterConverterArgs))
{
pbinding.ConverterParameter = new ParameterConverterArgs()
{
OriginalConverter = pbinding.Converter,
OriginalParameter = pbinding.ConverterParameter,
ParameterIndex = i
};
pbinding.Converter = this;
pbinding.Mode = BindingMode.OneWay;
}
mbinding.Bindings.Add(pbinding);
}
_evaluatedParameters.Add(pvalue);
}
object value = mbinding.ProvideValue(serviceProvider);
_multiBindingExpression = value as MultiBindingExpression;
return value;
}
The main work is performed in IMultiValueConverter.Convert
method.
It is called from MultiValueExpression
when a final value must be received.
Here we can use evaluated parameters to create another Binding
, set PathParameters
with these parameters and get the final value using BindingOperations
class
on target object.
To evaluate the final value, we can use our own attached property that will
accept the binding and will return the value. We should be careful here, because
it is possible that for the same target object more than one dependency property
could be bound with our extension (in this case we should support several
attached properties).
object IMultiValueConverter.Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
DependencyObject targetObject = values[1] as DependencyObject;
if (targetObject == null)
{
return null;
}
if (_evaluationProperty == null)
{
_evaluationProperty = PathEvaluationProperties.GetFreeEvaluationProperty(targetObject, this);
}
PathEvaluationBinding binding = BindingOperations.GetBindingBase(targetObject, _evaluationProperty) as PathEvaluationBinding;
if (binding == null)
{
binding = new PathEvaluationBinding(this, targetObject);
binding.Path = new PropertyPath(_path, _parameters.ToArray());
if (_multiBindingExpression != null)
{
binding.Mode = _multiBindingExpression.ParentMultiBinding.Mode;
}
if (binding.Mode == BindingMode.Default)
{
if (_targetProperty != null)
{
FrameworkPropertyMetadata mt = _targetProperty.GetMetadata(_targetObject) as FrameworkPropertyMetadata;
if (mt != null && mt.BindsTwoWayByDefault)
{
binding.Mode = BindingMode.TwoWay;
}
}
}
if (string.IsNullOrEmpty(_elementName))
{
binding.Source = _source;
}
else
{
binding.ElementName = _elementName;
}
}
if (_parametersChanged)
{
_parametersChanged = false;
for (int i = 0; i < _evaluatedParameters.Count; i++)
{
binding.Path.PathParameters[i] = _evaluatedParameters[i];
}
}
try
{
_disableNotification = true;
BindingOperations.SetBinding(targetObject, _evaluationProperty, binding);
}
finally
{
_disableNotification = false;
}
object value = binding.EffectiveValue;
if (value != null)
{
if (!targetType.IsAssignableFrom(value.GetType()))
{
TypeConverter tc = TypeDescriptor.GetConverter(value);
value = tc.ConvertTo(value, targetType);
}
}
return value;
}
Here I described
the simplified version of implementation. The real implementation could be downloaded
using the link above and contains support for OneWay and TwoWay bindings.
History
28/03/2012 - First Version