Thursday, February 15, 2007

How small can you go? - Part II

Updated: 19 March 2007 to resolve some errors

In my last post ("How small can you go?" 22 Jan 2007) I presented a custom control that allowed you to determine if the control was below a specific size, and to alter its visual appearance based upon this. There are many cases however where altering appearance based upon size is important (for example, to make the thumb grips on scrollbars disappear when they become too small), and I felt a more generic method of performing this calculation was required.

Ideally this method would not require a special base class as described in my previous post. It should also be as general as possible, allowing bindings to any available dependency properties.

So let us consider how we currently implement triggers,

<Trigger Property="IsMouseOver" Value="True">
...
</Trigger>

<Trigger Property="Width" Value="150">
...
</Trigger>


The first of these examples makes perfect sense. The IsMouseOver property is either true or false. We apply the default style when it is false, and apply any changes specified within the Trigger when it is true. However, the Width property does not have a small number of distinct values. Whilst the above trigger will apply if the width equals 150, it will not if it is 149,148,147,etc. or 151,152,153,etc. If only we had a less-than trigger...


Now let me introduce the IValueConverter interface. This according to the MSDN documentation "Provides a way to apply custom logic to a binding." Basically it allows you to include some logic that alters the value of the property before the trigger performs its comparison.


For example,



<DataTrigger Value="True" Binding="{Binding RelativeSource={RelativeSource Self}, Path=ActualWidth, Converter={StaticResource addTenConverter}, ConverterParameter=150}">
...
</DataTrigger>


Here we specify a custom converter, whose logic simply adds ten to a value. Therefore if the Width of our element is 140, the binding will first call our converter, which will of course return 150. Since this matches the Value attribute, the trigger will be applied. The converter just sits between the Property and the comparison to Value and provides some conversion.


So how does this help us? Well, there is one more attribute we need to introduce. The ConverterParameter attribute allows us to supply an additional parameter to our converter when it does its stuff. Now we can envisage,



<... .Resources>
<cvt:LessThanConverter x:Key="LessThanConverter"/>
</... .Resources>

<DataTrigger Value="True" Binding="{Binding RelativeSource={RelativeSource Self}, Path=ActualWidth, Converter={StaticResource lessThanConverter}, ConverterParameter=200}">
...
</DataTrigger>


Note that we create an instance of the converter as a static resource. The actual code behind the converter is relatively straightforward. All we do is take the supplied value, compare it to the supplied parameter, and return true if the value is less otherwise return false. In fact this is slightly more complex. Whilst a ValueConversionAttribute may be applied to the converter class specifying the type for the parameter, when written in XAML as shown above this is ignored and a string is supplied anyway. Therefore the resulting code is,



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

[ValueConversion(
typeof(double), typeof(bool), ParameterType = typeof(double))]
public class LessThanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
double doubleValue = (double)value;
double doubleParameter;

if (parameter is double)
doubleParameter
= (double)parameter;
else if (parameter is string)
{
if (!Double.TryParse((string)parameter, out doubleParameter))
throw new FormatException("The parameter for this LessThanConverter could not be converted to a System.Double");
}
else
throw new ArgumentException("The parameter for this LessThanConverer is of an unsupported type", "parameter");

Console.WriteLine(
"{0} < {1}", doubleValue, doubleParameter);

return doubleValue < doubleParameter;
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return new NotSupportedException();
}
}


So now we can trigger when any dependency property that is defined as a double is less than a specified value. The original problem from part 1 however involved changing the appearance of a window when either of the Width or Height properties were below a certain threshold. We can specify two separate triggers, but this requires duplication of all the setters within them. What we really need is some way to apply a 'logical OR' operation to two or more triggers. Next time... The IMultiValueConverter interface.



Technorati tags: , ,

2 comments:

Anonymous said...

Andy, great post, seems like it should work, however, it looks like Trigger doesn't have a "Converter" attribute....

Unknown said...

Thanks for that comment. The code above was adapted from a more complex example and I put the converter in the wrong place. Sorry, my mistake! I have corrected the post now so it should work as expected.

The 'Converter' parameter is actually an attribute on the Binding, not the Trigger (as my original post showed). Hence, we need to use a DataTrigger and bind to the desired property.

Have a look at the updated post and it should make more sense now.