Sunday, April 15, 2007

How small can you go? - Part III

In part one of this series of posts ("How small can you go?" 22 Jan 2007) I described how you could design a Windows Presentation Foundation (WPF) control that altered the visibility of specific areas of a UI when then containing window was resized. Compare this to the behaviour of the Office 2007 ribbon, which will hide itself to show more of the edited document when the window is below a certain size. In part II ("How small can you go? - Part II" 15 Feb 2007) I detailed an alternative approach that allowed a trigger to be used when any bindable property (e.g. the width of a window) was below a certain size. In this post I will complete the implementation.


To summarise part II, we implemented an IValueConverter that would return a boolean value indicating whether a specified binding was less than a certain value. This could be used as follows,



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

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


While this allowed defining a trigger for when the width was below a specific threshhold, we would have to define the trigger again for the height of the window. What we really need is some way to combine two bindings with a 'logical OR' operation.


Introducing the IMultiValueConverter...


An IMultiValueConverter is similar in principle to an IValueConverter, whereas the latter allows the conversion of a single value into another form (in our case, a double into a bool indicating whether the value is below a threshold), the former will take several values, and combine them into a single output value that is then used as a trigger. We therefore need an IMultiValueConverter that will take a number of booleans and output a single boolean that represents a 'logical OR' of all the input values. We can then use the following XAML,



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

...

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



This snippet declares two objects, our LessThanConverter from before, and a new MultiValueOrConverter. Following this we have a DataTrigger who's binding is a MultiBinding that takes two bindings that will return true if the width or height is less than 200. By default the MultiBinding will only trigger if all the child bindings are true (a 'logical AND'). We change this behaviour to a 'logical OR' by specifying our multi-value converter. The trigger will now fire if either the element width OR height are less than 200.


The actual code for the multi-value converter is similar in principle to that of a single-value converter. We take an array of input values that we will assume are all boolean values. We then perform a foreach loop over them and if any are true, we return true, otherwise false. Therefore the resulting code is,



using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Data;
using System.Globalization;

namespace SizingApplication2
{
public class MultiValueOrConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
foreach (bool value in values)
{
if (value == true)
return true;
}

return false;
}

public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}

}
}



If we combine this with the XAML snippet above and include a suitable setter to hide the desired parts of the UI, we now have all the behaviour we require. Advantages of this approach include not requiring any extra elements, and being able to adapt the value converters to any situation where the UI changes based upon a certain value (for example, to display a warning message when a slider is moved above a certain value, or even change its colour as a warning).


The full code for this post and an example usage is available here.