01 August 2009

Bind Silverlight 3 Slider Value, Minimum and Maximum attributes in EXACTLY the right order!

Today I want to share a piece of very hard won knowlegde with you. That piece of knowlegde is: if you want to use a Silverlight Slider, and you want to get the Value, Minimum and Maximum attributes from binding, you need to bind them in exactly the right order. That order is:
  • Maximum
  • Minimum
  • Value
or else it simply won't work. It took me the better part of a sunny saturday afternoon to figure that out. To clarify things, I will show what I was doing when I ran into this. I have a simple business class that looks like this:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;

namespace LocalJoost.CloudMapper.Ui
{
  public class TileRequestParms 
  {
    private double _xmin;
    public double XMin { get { return _xmin; } }

    private double _ymin;
    public double YMin { get { return _ymin; } }

    private double _xmax;
    public double XMax { get { return _xmax; } }

    private double _ymax;
    public double YMax { get { return _ymax; } }

    private double _xLow;
    [Display(Name = "X lower left:")]
    public double XLow
    {
      get
      {
        return _xLow;
      }
      set
      {
        ValidateValue(value, XMin, XHigh);
        _xLow = value;
      }
    }

    private double _yLow;
    [Display(Name = "Y lower left:")]
    public double YLow
    {
      get
      {
        return _yLow;
      }
      set
      {
        ValidateValue(value, YMin, YHigh);
        _yLow = value;
      }
    }

    private double _xHigh;
    [Display(Name = "X upper right:")]
    public double XHigh
    {
      get
      {
        return _xHigh;
      }
      set
      {
        ValidateValue(value, XLow, XMax);
        _xHigh = value;

      }
    }

    private double _yHigh;
    [Display(Name = "Y upper right:")]
    public double YHigh
    {
      get
      {
        return _yHigh;
      }
      set
      {
        ValidateValue(value, YLow, YMax);
        _yHigh = value;
      }
    }

    public TileRequestParms(double xLow, double yLow, 
        double xHigh, double yHigh)
    {
      _xmax = _xHigh = xHigh;
      _ymax = _yHigh = yHigh;
      _xmin = _xLow = xLow;
      _ymin = _yLow = yLow;
    }

    private void ValidateValue(double value, 
      double minValue, double maxValue)
    {
      if (value < minValue || value > maxValue)
      {
        throw new ArgumentException(
           string.Format("Valid value {0} - {1}", minValue,
                             maxValue));
      }
    }
  }
}
Its purpose is very simple: the user can input two coordinates that are inside a rectangle defined by the max and min values in the constructor. In addition, XMax can never be smaller than XMin, and YMax never smaller than YMin. Now I wanted the user to be able to input the value both by a text box and a slider. And I decided to use the new Silverlight 3 DataForm and control to control binding:
<dataFormToolkit:DataField Grid.Row="0" Grid.Column="0" 
  Label="X lower left: ">
    <StackPanel>
        <TextBox x:Name ="tbXLow" Text="{Binding XLow,Mode=TwoWay}" />
        <Slider x:Name="slXLow" 
         Value="{Binding Text,ElementName=tbXLow, Mode=TwoWay}" 
         Minimum="{Binding XMin}" Maximum="{Binding XMax}"/>
    </StackPanel>
</dataFormToolkit:DataField>
<dataFormToolkit:DataField Grid.Row="0" Grid.Column="1" 
      Label="Y lower left: ">
    <StackPanel>
        <TextBox x:Name ="tbYLow" Text="{Binding YLow,Mode=TwoWay}" />
        <Slider x:Name="slYLow" 
         Value="{Binding Text,ElementName=tbYLow, Mode=TwoWay}" 
         Minimum="{Binding YMin}" Maximum="{Binding YMax}"/>
    </StackPanel>
</dataFormToolkit:DataField>
<dataFormToolkit:DataField Grid.Row="1" Grid.Column="0" 
    Label="X upper right: ">
    <StackPanel>
        <TextBox x:Name ="tbXHigh" Text="{Binding XHigh,Mode=TwoWay}" />
        <Slider x:Name="slXHigh" 
         Value="{Binding Text,ElementName=tbXHigh, Mode=TwoWay}" 
         Minimum="{Binding XMin}" Maximum="{Binding XMax}"/>
    </StackPanel>
</dataFormToolkit:DataField>
<dataFormToolkit:DataField Grid.Row="1" Grid.Column="1" 
  Label="Y upper right: ">
    <StackPanel>
        <TextBox x:Name ="tbYHigh" Text="{Binding YHigh,Mode=TwoWay}" />
        <Slider x:Name="slYHigh" 
         Value="{Binding Text,ElementName=tbYHigh, Mode=TwoWay}" 
         Minimum="{Binding YMin}" Maximum="{Binding YMax}"/>
    </StackPanel>
</dataFormToolkit:DataField>
Now the annoying thing is: is worked for X, but not for Y. The sliders stayed put and refused to work. After a considerable time of debugging and I noticed one thing: it worked when I put hard coded values for Minimum and Maximum, but not if I used binded values. For X, I was using a minimum 0 and a maximum of 300000. But I was using a minimum of 300000 for Y, and 600000 for a maximum (Yes, that's the RD coordinate system, for my Dutch readers). With a bit of debugging I was able to find out the both minimum and maximum value of the first slider where set to 300000, as was it's value. So I reasoned: maybe, by setting the value, the maximum value is set already (because the minimum is already 0) and it does 'not like' to get a max value twice. Or something among that lines. So what if I first set the maximum, then the minimum, and only then the value? So I changed the XAML for the first slider
<dataFormToolkit:DataField Grid.Row="1" Grid.Column="0" 
  Label="X upper right: ">
    <StackPanel>
        <TextBox x:Name ="tbXHigh" Text="{Binding XHigh,Mode=TwoWay}" />
        <Slider Maximum="{Binding XMax}" Minimum="{Binding XMin}"
         x:Name="slXHigh" 
         Value="{Binding Text,ElementName=tbXHigh, Mode=TwoWay}"/>
    </StackPanel>
</dataFormToolkit:DataField>
And it worked. It's these little things that make a developer's life interesting, isn't it? Although, in my case, it also gave this here developer a pretty potent headache.

2 comments:

Unknown said...

Well, there seems some logic in here when you really com to think of it. When you assign a value (through binding) to the Minimum that is greater than the value of the Maximum property, apparently the Slider is intelligent enough to react to that, albeit in a confusing way.
It makes very much sense to first set the Maximum, especially when the Minimum is higher than the original value of Maximum.

Same goes for the Value, as it should be within the Minimum-Maximum range.

But this is all just the easy thinking after you had the problem and the problem fixed. This can cost you hours.

Joost van Schaik said...

Oh sure - the logic is clear. But in XML the attribute order is supposed to be insignificant. In XAML - at least for the Slider - this not seems to be the case.