views:

379

answers:

3

How to skip updating some of the sub-bindings of a MultiBinding? I have defined in code-behind (I had some troubles making it in XAML and I don't think it matters - after all code-behind is not less expressive then XAML) a MultiBinding which takes two read-only properties and one normal property to produce a single value. In case of ConvertBack the read-only properties are not modified (they sustain their value) and only the normal property is changed.

While defining the MultiBinding the entire MultiBinding was set to TwoWay however particular sub-bindings where set appropriate (first two to OneWay and the third two TwoWay).


The problem occurs in a my own control. However for the sake of presentation I simplified it to a smaller control. The control presented in this example is a Slider-like control allowing to select a value in [0.0; 1.0] range. The selected value is represented by the thumb and exposed as a DependencyProperty.

Basically the control is build by a 1 row x 3 column Grid where the thumb is in the middle column. To correctly position the thumb left column must be assigned width corresponding to selected position. However this width depends also on the actual width of the entire control and actual width of the thumb itself (this is because the position is given as a relative value in [0.0; 1.0] range).

When the thumb is moved the position should be updated appropriately however the thumb width and control width obviously do not change.

The code works as expected however when run in IDE during thumb moving Output window is cluttered with exceptions information as reported when MultiBinding tries to set value to those two read-only properties. I suspect it is not harmful however it is somewhat annoying and misleading. And also it means that the code does something else then I wanted it to do as I didn't want to set those properties (this matters in case they were not read-only and this would actually modify them).

MultiBinding documentation in Remarks section mentions that individual sub-bindings are allowed to override the MultiBinding mode value but it doesn't seem to work.

Maybe this could be solved somehow by expressing the dependency on the control and thumb widths (the read-only properties) somehow differently. For example registering to their notifications separately and enforcing update upon their change. However it does not seem natural to me. MultiBinding does on the other hand as after all left column width does depend on those three properties.


Here is the example XAML code.

<UserControl x:Class="WpfTest.ExampleUserControl"
 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"&gt;
 <Grid>
  <Grid.RowDefinitions>
   <RowDefinition />
  </Grid.RowDefinitions>
  <Grid.ColumnDefinitions>
   <ColumnDefinition x:Name="leftColumn" />
   <ColumnDefinition x:Name="thumbColumn" Width="Auto" />
   <ColumnDefinition />
  </Grid.ColumnDefinitions>
  <!-- Rectangle used in the left column for better visualization. -->
  <Rectangle Grid.Column="0">
   <Rectangle.Fill>
    <LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
     <GradientStop Color="Black" Offset="0" />
     <GradientStop Color="White" Offset="1" />
    </LinearGradientBrush>
   </Rectangle.Fill>
  </Rectangle>
  <!-- Thumb representing the Position property. -->
  <GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Center" />
  <!-- Rectangle used in the right column for better visualization. -->
  <Rectangle Grid.Column="2">
   <Rectangle.Fill>
    <LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
     <GradientStop Color="White" Offset="0" />
     <GradientStop Color="Black" Offset="1" />
    </LinearGradientBrush>
   </Rectangle.Fill>
  </Rectangle>
 </Grid>
</UserControl>

And here is the corresponding code-behind

using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace WpfTest
{
 public partial class ExampleUserControl : UserControl
 {
  #region PositionConverter

  private class PositionConverter : IMultiValueConverter
  {
   public PositionConverter(ExampleUserControl owner)
   {
    this.owner = owner;
   }

   #region IMultiValueConverter Members

   public object Convert(
    object[] values,
    Type targetType,
    object parameter,
    CultureInfo culture)
   {
    double thisActualWidth = (double)values[0];
    double thumbActualWidth = (double)values[1];
    double position = (double)values[2];

    double availableWidth = thisActualWidth - thumbActualWidth;

    double leftColumnWidth = availableWidth * position;

    return new GridLength(leftColumnWidth);
   }

   public object[] ConvertBack(
    object value,
    Type[] targetTypes,
    object parameter,
    CultureInfo culture)
   {
    double thisActualWidth = owner.ActualWidth;
    double thumbActualWidth = owner.thumbColumn.ActualWidth;
    GridLength leftColumnWidth = (GridLength)value;

    double availableWidth = thisActualWidth - thumbActualWidth;

    double position;
    if (availableWidth == 0.0)
     position = 0.0;
    else
     position = leftColumnWidth.Value / availableWidth;

    return new object[] {
     thisActualWidth, thumbActualWidth, position
    };
   }

   #endregion

   private readonly ExampleUserControl owner;
  }

  #endregion

  public ExampleUserControl()
  {
   InitializeComponent();

   MultiBinding leftColumnWidthBinding = new MultiBinding()
   {
    Bindings =
    {
     new Binding()
     {
      Source = this,
      Path = new PropertyPath("ActualWidth"),
      Mode = BindingMode.OneWay
     },
     new Binding()
     {
      Source = thumbColumn,
      Path = new PropertyPath("ActualWidth"),
      Mode = BindingMode.OneWay
     },
     new Binding()
     {
      Source = this,
      Path = new PropertyPath("Position"),
      Mode = BindingMode.TwoWay
     }
    },
    Mode = BindingMode.TwoWay,
    Converter = new PositionConverter(this)
   };
   leftColumn.SetBinding(
    ColumnDefinition.WidthProperty, leftColumnWidthBinding);
  }

  public static readonly DependencyProperty PositionProperty =
   DependencyProperty.Register(
    "Position",
    typeof(double),
    typeof(ExampleUserControl),
    new FrameworkPropertyMetadata(0.5)
   );

  public double Position
  {
   get
   {
    return (double)GetValue(PositionProperty);
   }
   set
   {
    SetValue(PositionProperty, value);
   }
  }

 }
}
+2  A: 

It looks like MultiBinding doesn't work right. I've seen some unexpected behavior (something like yours) before in my practice. Also you can insert breakpoints or some tracing in converter and you can find some funny things about which converters and when are called. So, if its possible, you should avoid using MultiBinding. E.g. you can add special property in your view model that will set value of your mutable property in its setter and return needed value using all three your properties in its getter. Its something like a MultiValueConverter inside a property =).

Hope it helps.

levanovd
If this really is some kind of bug in MultiBinding then maybe we should report it somehow. Where to do this?
Adam Badura
Did you try to do the same in .NET 4.0? Maybe it is fixed now. (I can not try now because of some technical problems with my VS 2010).
levanovd
A: 

Hi Adam,

To report bugs, please use https://connect.microsoft.com.

Thanks, Carole

Carole
This should be a comment instead. However I guess you were not able to add comments with your current Reputation.
Adam Badura
+2  A: 

Finally I found the solution myself. Actually it is in the documentation - I don't know how I missed that but I paid dearly (in wasted time) for it.

According to the documentation ConvertBack ought to return Binding.DoNothing on positions on which no value is to be set (in particular there were OneWay binding is desired). Another special value is DependencyProperty.UnsetValue.

This is not a complete solution as now IMultiValueConverter implementation must know where to return a special value. However I think most reasonable cases are covered by this solution.

Adam Badura