Click here to Skip to main content
15,867,568 members
Articles / Desktop Programming / WPF
Article

Using path parameters when binding data in WPF

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
28 Mar 2012CPOL4 min read 92.5K   1.3K   14  
Extends WPF framework with binding that supports runtime path parameters

Sample Image

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:

C#
// data model
public class Sensor
{
    public string Name { get; set; }
}
C#
// view model
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:

XML
<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:

  1. Parameters specified by user will be automatically evaluated during the binding.
  2. Our extension can be used in DataTrigger where custom markup extensions are not allowed.
  3. Support for two way bindings.
  4. 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.

C#
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.

C#
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;

    // create wpf binding
    MultiBinding mbinding = new MultiBinding();
    mbinding.Mode = _mode;
    mbinding.Converter = this;

    // binding to evaluate data context
    Binding binding1 = new Binding();
    binding1.Mode = BindingMode.OneWay;
    mbinding.Bindings.Add(binding1);

    // binding to evaluate target element
    Binding binding2 = new Binding();
    binding2.Mode = BindingMode.OneWay;
    binding2.RelativeSource = new RelativeSource(RelativeSourceMode.Self);
    mbinding.Bindings.Add(binding2);

    // binding to private property, that will reevaluate final value when this property changes
    Binding binding3 = new Binding();
    binding3.Mode = BindingMode.OneWay;
    binding3.Source = this;
    binding3.Path = new PropertyPath("EffectiveValueChanged");
    mbinding.Bindings.Add(binding3);

    // this will hold evaluated parameters
    _evaluatedParameters = new Collection<object>();

    // for every binding parameter apply our internal converter
    for (int i = 0; i < _parameters.Count; i++)
    {
        object pvalue = _parameters[i];

        if (pvalue is Binding)
        {
            Binding pbinding = pvalue as Binding;

            // apply only once
            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).

C#
object IMultiValueConverter.Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
    DependencyObject targetObject = values[1] as DependencyObject;
    if (targetObject == null)
    {
        return null;
    }

    // to allow several dependency properties to be bound to single target object
    // use several attached properties, here just find the next available property to use
    // available, means no binding is set to this property
    if (_evaluationProperty == null)
    {
        _evaluationProperty = PathEvaluationProperties.GetFreeEvaluationProperty(targetObject, this);
    }

    // create path evaluation binding
    PathEvaluationBinding binding = BindingOperations.GetBindingBase(targetObject, _evaluationProperty) as PathEvaluationBinding;

    if (binding == null)
    {
        binding = new PathEvaluationBinding(this, targetObject);
        binding.Path = new PropertyPath(_path, _parameters.ToArray());

        // set binding mode according to the specified in extension by user or in property metadata
        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
    {
        // notifications are sent when source value is changed and binding is TwoWay or OneWay
        // when we set binding here, notification is sent also, so disable it to prevent an infinite loop
        _disableNotification = true;

        BindingOperations.SetBinding(targetObject, _evaluationProperty, binding);
    }
    finally
    {
        _disableNotification = false;
    }

    object value = binding.EffectiveValue;

    // now we have to convert the value
    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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Founder eMightGen Ltd.
Israel Israel
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --