Impossible love

Dedicated to the endless frustrations of trying to make these damn machines do what we want them to do...

Dedicated to the endless frustrations of trying to make these damn machines do what we want them to do...

Daniel Stolt's Blog

Let him who is without sin DirectCast the first Object.

Translating Numeric Values Between Different Scales in WPF Data Binding

Categories: Programming | English   Tags: | |

A while back I wrote a WPF application in which I wanted to bind several things to the value of a Slider control. In this case, I wanted the Slider to control both the size and the opacity of an Image, so that when the user moved the slider from right to left, the Image would become smaller and fade out of view. The problem was of course that the value of the Slider could not be made to directly correspond to the to the value of both the size and the opacity of the Image, because those values were on a different scale.

To put this into more concrete terms, the opacity of the Image must be specified as a value between zero and one, while I wanted the size of the Image to vary between 100 and 500 pixels (both width and height). So, no matter what I use as the Minimum and Maximum values of the Slider, there is no way I can directly bind both the opacity and the size of the Image to the value of the Slider. I needed some way to translate the value of the Slider onto a different scale for each of the bindings.

I quickly realized this was a general problem, looked through the WPF documentation and did some googling on the subject, came up empty and therefore, as we programmers like to do, set out to build a generic solution. Now, I don’t consider myself a WPF expert, and I suspect there might be a simpler way to do this. If there is, I’d love to hear about it. Nonetheless, here is a brief run-down of what I did in case it might help somebody facing the same problem.

IValueConverter to The Rescue

In WPF data binding, there is the concept of a value converter, which is a mechanism by which we can write custom code to convert values between the source and target end of a binding. This mechanism is implemented by means of the IValueConverter interface and the ValueConversionAttribute.

Here’s a few words form the documentation:

If you want to associate a value converter with a binding, create a class that implements the IValueConverter interface and then implement the Convert and ConvertBack methods. Converters can change data from one type to another, translate data based on cultural information, or modify other aspects of the presentation.

There are a few built-in converter classes in WPF which implement the IValueConverter interface:

  • AlternationConverter
  • BooleanToVisibilityConverter
  • ZoomPercentageConverter
  • JournalEntryListConverter

None of them are applicable to our scenario. Clearly, we need another type of converter that will allow us to do arbitrary translation of values fron one scale onto another, and that will allow us to specify the source and destination scales on a per-binding basis.

A Reusable Translator Class

So, let’s create a class that implements IValueConverter and that maps the source value of the binding on one scale to the destination value of the binding on another scale. Let’s call the class Translator for simplicity. To begin with, here’s code for Translator in its entirety:

   1: Imports System.Globalization
   2: Imports System.Windows.Data
   3:  
   4:  
   5: <ValueConversion(GetType(Double), GetType(Double))> _
   6: Public Class Translator
   7:     Implements IValueConverter
   8:  
   9:  
  10:     Sub New()
  11:         m_SrcMin = 0
  12:         m_SrcMax = 1
  13:         m_DstMin = 0
  14:         m_DstMax = 1
  15:     End Sub
  16:  
  17:  
  18:     Sub New(ByVal srcMin As Double, ByVal srcMax As Double, _
  19:             ByVal dstMin As Double, ByVal dstMax As Double)
  20:         m_SrcMin = srcMin
  21:         m_SrcMax = srcMax
  22:         m_DstMin = dstMin
  23:         m_DstMax = dstMax
  24:     End Sub
  25:  
  26:  
  27:     Private m_SrcMin As Double
  28:     Private m_SrcMax As Double
  29:     Private m_DstMin As Double
  30:     Private m_DstMax As Double
  31:  
  32:  
  33:     Public Property SrcMin() As Double
  34:         Get
  35:             Return m_SrcMin
  36:         End Get
  37:         Set(ByVal value As Double)
  38:             m_SrcMin = value
  39:         End Set
  40:     End Property
  41:  
  42:  
  43:     Public Property SrcMax() As Double
  44:         Get
  45:             Return m_SrcMax
  46:         End Get
  47:         Set(ByVal value As Double)
  48:             m_SrcMax = value
  49:         End Set
  50:     End Property
  51:  
  52:  
  53:     Public Property DstMin() As Double
  54:         Get
  55:             Return m_DstMin
  56:         End Get
  57:         Set(ByVal value As Double)
  58:             m_DstMin = value
  59:         End Set
  60:     End Property
  61:  
  62:  
  63:     Public Property DstMax() As Double
  64:         Get
  65:             Return m_DstMax
  66:         End Get
  67:         Set(ByVal value As Double)
  68:             m_DstMax = value
  69:         End Set
  70:     End Property
  71:  
  72:  
  73:     Public Function Convert(ByVal value As Object, ByVal targetType As System.Type, _
  74:                             ByVal parameter As Object, ByVal culture As CultureInfo) _
  75:                             As Object Implements IValueConverter.Convert
  76:         Dim src As Double = CDbl(value)
  77:         Dim dst As Double
  78:         If src < m_SrcMin Then
  79:             dst = m_DstMin
  80:         ElseIf src > m_SrcMax Then
  81:             dst = m_DstMax
  82:         Else
  83:             dst = (src - m_SrcMin) / (m_SrcMax - m_SrcMin) * (m_DstMax - m_DstMin) + m_DstMin
  84:         End If
  85:         Return dst
  86:     End Function
  87:  
  88:  
  89:     Public Function ConvertBack(ByVal value As Object, ByVal targetType As System.Type, _
  90:                                 ByVal parameter As Object, ByVal culture As CultureInfo) _
  91:                                 As Object Implements IValueConverter.ConvertBack
  92:         Dim dst As Double = CDbl(value)
  93:         Dim src As Double
  94:         If dst < m_DstMin Then
  95:             src = m_SrcMin
  96:         ElseIf dst > m_DstMax Then
  97:             src = m_SrcMax
  98:         Else
  99:             src = (dst - m_DstMin) / (m_DstMax - m_DstMin) * (m_SrcMax - m_SrcMin) + m_SrcMin
 100:         End If
 101:         Return src
 102:     End Function
 103:  
 104:  
 105: End Class

Now let’s walk though this code from the top.

First of all, Translator needs to be decorated with the ValueConversionAttribute to let WPF and designer tools such as Expression Blend know what specific types this converter converts between. Ideally, Translator would be a generic class where the type of the value could be specified as a type parameter, but there are a number of reasons why this is difficult to achieve in this case:

  1. Instantiating a generic type from XAML markup is not possible, at least to my knowledge.
  2. The type parameter would need to be specified in the ValueConversionAttribute above our class but .NET does not allow for such a construct.
  3. There is no mechanism by which we can limit the type parameter to numeric types on which the arithmetics in our code can be performed.

So, we are limited to using System.Double as the type on which Translator can operate. This is fine, because most numeric properties in WPF that we might need to translate in this manner are indeed exposed as System.Double.

Second, we need some properties and constructors. The source and destination scales can be specified using their respective min and max values, so besides implementing the methods of IValueConverter, our class needs 4 properties. The default constructor initializes those with reasonable (but mostly useless) default values. We also provide a constructor where these values can be specified. This is convenient when you want to instantiate Translator from code instead of markup.

Third, we need to implement the Convert and ConvertBack methods of IValueConverter to perform the actual translation. One thing to note here is that this code also truncates the source and destination values to be within the minimum and maximum values specified for their respective scales. This is something you might want to do differently in your own implementation, opting instead to allow linear projection of the values outside the specified minimum and maximum.

Using Translator from XAML

Allright, now that we have our Translator, let’s put it to some use in XAML. First of all we need to declare an XML namespace that maps to whatever CLR namespace in which Translator happens to reside. In my case it lives in the namespace Perceptible.Utils.Wpf, so I add the following namespace declaration to my Window:

   1: <Window
   2:     ...
   3:     xmlns:utils="clr-namespace:Perceptible.Utils.Wpf">

Next we need to create a couple of instances of Translator to do the translation for us. Remember, in my case I want to bind the size and opacity of an Image to the value of a Slider, so I need two Translator instances each with different destination scales. The easiest way to create those is to add them to the resource collection of the Window:

   1: <Window.Resources>
   2:     <utils:Translator x:Key="OpacityTranslator" SrcMin="0" SrcMax="1" DstMin="0" DstMax="1"/>
   3:     <utils:Translator x:Key="SizeTranslator" SrcMin="0" SrcMax="1" DstMin="100" DstMax="500"/>
   4: </Window.Resources>

Finally, to tie it all together, we just need to specify that these Translator instances be used as value converters in our bindings. We want to bind the Opacity, Width and Height properties of the Image element to the Value property of the Slider, but using different value translation. If you’re using Expression Blend there’s nice GUI support to do this, but in case you’re not here’s the appropriate markup to do the binding (all the other attributes of the Image element are omitted for clarity):

   1: <Slider x:Name="FadeSlider" Minimum="0" Maximum="1" Value="1" />
   2: <Image Opacity="{Binding Path=Value, Converter={StaticResource OpacityTranslator}, ElementName=FadeSlider, Mode=Default}"
   3:        Width="{Binding Path=Value, Converter={StaticResource SizeTranslator}, ElementName=FadeSlider, Mode=Default}"
   4:        Height="{Binding Path=Value, Converter={StaticResource SizeTranslator}, ElementName=FadeSlider, Mode=Default}" />

That should be it. Hope someone out there finds this useful! And like I said, if you know of another way to accomplish this, I would love to hear about it, so leave a comment or drop me a message.

Calendar

<<  September 2010  >>
MoTuWeThFrSaSu
303112345
6789101112
13141516171819
20212223242526
27282930123
45678910

View posts in large calendar

Recent comments

Comment RSS