Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TwoWay MultiBinding with read-only properties

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">
 <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);
   }
  }

 }
}
like image 606
Adam Badura Avatar asked Nov 19 '09 23:11

Adam Badura


2 Answers

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.

like image 135
Adam Badura Avatar answered Oct 24 '22 02:10

Adam Badura


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.

like image 30
levanovd Avatar answered Oct 24 '22 04:10

levanovd