Click here to Skip to main content
15,867,686 members
Articles / Desktop Programming / WPF

WPF Collapse Converters for easier data layouts

Rate me:
Please Sign up or sign in to vote.
4.85/5 (8 votes)
8 Oct 2009Ms-PL7 min read 48.8K   1.9K   28   1
This article demonstrates how WPF Converters can be used in an innovate way to replace the use of WPF Triggers in a common user interface task.

Introduction

This article demonstrates how WPF Converters can be used in an innovate way to replace the use of WPF Triggers in a common user interface task. The code contains a small set of self contained classes that can be easily added to any WPF project.

Background

A common design issue when presenting complex data is how much detail to display. Too much detail, and the user may get confused or find it more difficult to find the item that they are looking for. Too little, and the user may not have all the information that they need. In the past, many of these decisions were fixed at design time. Modern user interfaces dynamically hide and display additional data through user actions.

This example uses a product list from a fictional bike company. Products have a code, name, price, description, optional photo, and a sale ended date. The data is taken from the SQL Server AdventureWorks sample database. All the data is contained in the code so there is no need to use an external database to run the example code.

Image 1

The example application consists of a single window containing an ordinary list box filled with a list of objects from the Product class. A WPF DataTemplate is used to format each product in the list. This sort of layout can be found in many WPF samples and tutorials. What makes it more interesting is the checkboxes on the window that dynamically control how much information is displayed.

Image 2

In the second screenshot, the images and prices have been hidden and only the currently selected product has its description shown. There are additional automatic features as well. If an item has a blank Sale Ended date, then the red line at the bottom is hidden, and if an image is not available, the image and its border are hidden.

The usual way of doing this

The data template contains controls to display each part of the product. Each UIElement has a Visibility property, and setting that to the value Collapsed hides the element and forces its container to layout the other controls as if it was not there. So, all that is needed is a means for setting the Visibility property under the right circumstances.

The usual way to do this is with a DataTrigger, as show in this code example:

XML
<TextBlock Margin="0,3,0,0" HorizontalAlignment="Left" 
       VerticalAlignment="Top" Width="Auto" Height="Auto" 
       TextWrapping="Wrap" FontStyle="Italic" TextDecorations="None"
       Grid.Column="1" Grid.ColumnSpan="1" Grid.Row="1" 
       TextAlignment="Left" Text="{Binding Path=Description}" 
       TextTrimming="WordEllipsis" FontSize="10" >
    <TextBlock.Style>
      <Style>
        <Style.Triggers>
          <DataTrigger
              Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, 
                       AncestorType={x:Type ListBoxItem} }, Path=IsSelected}"
              Value="False">
            <Setter Property="TextBlock.Visibility" Value="Collapsed" />
          </DataTrigger>
        </Style.Triggers>
      </Style>
    </TextBlock.Style>
</TextBlock>

This is essentially a giant if statement that says if the parent item is not selected then hide the description. I do not like triggers because they add programming logic to the user interface, which is not a good thing. They are also over complicated; the trigger code above takes no account of the 'Always Display Descriptions' checkbox; to do so would need an even more complicated definition.

The Collapse Converters

The great thing about WPF is that there are usually several ways to achieve the same thing, so I came up with an alternative way that uses converters instead of triggers.

Converters are used to convert one value type into another type during data binding. A classic case contained in the example code implements a converter that formats a C# DateTime into a string for display as the Sale End Date. Converters are C# classes that implement the IValueConverter interface.

Before describing the collapse converters in detail, I will show you how they are used.

C#
public partial class CollapseConverterWindow : Window
{
    public CollapseConverterWindow()
    {
        InitializeComponent();
        listBoxProducts.ItemsSource = 
          TWSampleWPFDatabase.SampleData.Instance.Products.Items;
    }
}

The code behind the example window is very minimal; the only thing it does is initialize the Window and set the ListBox's contents to an ObservableCollection of Product objects. The Product class and the same data is all contained in a separate sub-project that may be worth an article in itself, but all we need to know here is that the Product class has public properties for each item that we want to display.

XML
<Window x:Class="TWCollapseConverters.CollapseConverterWindow"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="clr-namespace:TWCollapseConverters"
      Title="Collapse Converter Example" Height="419" 
      Width="396" Loaded="Window_Loaded">
    <Window.Resources>
        <local:CollapseOnBlankConverter x:Key="collapseOnBlankConverter"/>
        <local:CollapseOnBlanksConverter x:Key="collapseOnBlanksConverter"/>
        <local:CollapseOnBlanksFirstPairOrConverter 
            x:Key="collapseOnBlanksFirstPairOrConverter"/>
        <local:DateToShortDateStringConverter x:Key="dateToShortDateStringConverter"/>
        <DataTemplate x:Key="ProductTemplate">
        .....
        </DataTemplate>
    </Window.Resources>
    <Grid Background="LightGray">
        <Grid.RowDefinitions>
            <RowDefinition Height="40" />
            <RowDefinition />
        </Grid.RowDefinitions>
        <StackPanel Orientation="Horizontal" Margin="3" 
                     HorizontalAlignment="Center">
            <CheckBox Height="16" Name="checkBox_AlwaysDisplayDescriptions" 
              IsChecked="True">Always Display Descriptions</CheckBox>
            <CheckBox Height="16" Margin="10,0,0,0" 
              Name="checkBox_DisplayImages" 
              IsChecked="True">Display Images</CheckBox>
            <CheckBox Height="16" Margin="10,0,0,0" 
              Name="checkBox_DisplayPrices" 
              IsChecked="True">Display Prices</CheckBox>
        </StackPanel>
        <ListBox Grid.Row="1" Name="listBoxProducts" 
          ItemTemplate="{StaticResource ProductTemplate}" 
          HorizontalContentAlignment="Stretch" />
    </Grid>
</Window>

The XAML window definition follows a simple layout, with the complexity contained in the DataTemplate. Instances of the converters are defined in the Windows.Resources section and then referenced in the DataTemplate.

Instead of listing the entire data template, the parts that demonstrate each of the converts is shown below.

CollapseOnBlankConverter

The simplest of the three collapse converters takes a single value and converts it to either Visible or Collapse.

XML
<StackPanel Orientation="Horizontal" 
   Visibility="{Binding ElementName=checkBox_DisplayPrices, 
               Path=IsChecked, Converter={StaticResource collapseOnBlankConverter}}" 
 Grid.ColumnSpan="2" HorizontalAlignment="Left">
    <TextBlock Text="Price: $" Margin="4,0,0,0" 
       Foreground="DarkGreen" FontWeight="Bold" />
    <TextBlock Text="{Binding Path=ListPrice}" 
      Margin="0,0,2,0" Foreground="DarkGreen" FontWeight="Bold" />
</StackPanel>

In this example, the visibility of the container for the Price text is bound to the value of the 'Display Prices' checkbox. The IsChecked property returns a boolean value which cannot be directly bound to a visibility field, but by specifying CollapseOnBlankConverter as the converter, the check value will be converted into Visible or Collapse.

XML
<StackPanel Grid.Column="1" Grid.Row="2" 
            Orientation="Horizontal" 
            Visibility="{Binding Path=SellEndDate, 
                        Converter={StaticResource collapseOnBlankConverter}}">
    <TextBlock Text="Sale Ended On: " 
      Margin="0,3,3,0" Foreground="DarkRed" FontWeight="Bold" />
    <TextBlock Text="{Binding Path=SellEndDate, 
                     Converter={StaticResource dateToShortDateStringConverter}}" 
      Margin="0,3,0,0" Foreground="DarkRed" FontWeight="Bold" />
</StackPanel>

In the second example, the visibility is bound to the value of the date, and this too is converted into Visible or Collapse. How the Converter interprets its input gives rise to its name. If a value can be considered blank, then it will return Collapse, otherwise it returns Visible. So a boolean false, a null reference, an empty string, a blank date, or the number zero are all considered blank, and so cause the element to be hidden.

CollapseOnBlanksConverter

The second collapse converter takes a list of inputs instead of a single input. Defining a multiple binding cannot be done via the inline notation in the previous example. Instead, it has to be defined the verbose way.

XML
<Border Margin="5" BorderThickness="1" 
  BorderBrush="DarkGray" HorizontalAlignment="Center" 
  VerticalAlignment="Top">
    <Border.Visibility>
        <MultiBinding  Converter="{StaticResource collapseOnBlanksConverter}">
            <MultiBinding.Bindings>
                <Binding ElementName="checkBox_DisplayImages" Path="IsChecked"  />
                <Binding Path="ThumbNailPhotoAvailable"  />
            </MultiBinding.Bindings>
        </MultiBinding>
    </Border.Visibility>
    <Image  Source="{Binding Path=ThumbNailPhoto}" />
</Border>

In this example, the visibility of the image border element is bound to both the 'Display Images' check box and the boolean property of Product that indicates if an image is available. The CollapseOnBlanksConverter checks all its inputs, and if any one is blank, it will return Collapse. That way, the image will be hidden if is not available or the user has turned images off with the checkbox. There is no limit to the number of conditions that you can set in this way. The Image element will not display anything if its source is not defined, so this converter may look unnecessary. However, the border will still be displayed around a blank image, and anyway the Product class returns a valid image.

CollapseOnBlanksFirstPairOrConverter

The third converter is a variation on the second in that it takes the combined value of the first two elements and compares those with the rest. E.g., CollapseOnBlanksConverter => (A && B && C && D) and CollapseOnBlanksFirstPairOrConverter => ((A || B) && C && D).

XML
<TextBlock Margin="0,3,0,0" HorizontalAlignment="Left" 
       VerticalAlignment="Top" Width="Auto" Height="Auto" 
       TextWrapping="Wrap" FontStyle="Italic" TextDecorations="None"
       Grid.Column="1" Grid.ColumnSpan="1" Grid.Row="1" 
       TextAlignment="Left" Text="{Binding Path=Description}" 
       TextTrimming="WordEllipsis" FontSize="10" >
    <TextBlock.Visibility>
        <MultiBinding  Converter="{StaticResource collapseOnBlanksFirstPairOrConverter}">
           <MultiBinding.Bindings>
               <Binding ElementName="checkBox_AlwaysDisplayDescriptions" Path="IsChecked"  />
               <Binding Path="IsSelected" 
                 RelativeSource="{RelativeSource Mode=FindAncestor, 
                                 AncestorType={x:Type ListBoxItem} }"   />
               <Binding Path="Description"  />
           </MultiBinding.Bindings>
        </MultiBinding>
    </TextBlock.Visibility>
</TextBlock>

So in this example, the logic is to always hide the description if it is blank, and to show it if the item is selected or if the 'Always Display Descriptions' checkbox is ticked. This does the same job and a lot more than the Trigger example given at the top of the article, and is I think a lot easier to define.

The C# code

Now that you have seen how they are used, all that is left is to show you the Converters code implementation. The code is in a single C# file: TWCollapseConvertersLib.cs, which is part of the main project. It could be put in its own library, but it's too small to bother.

CollapseOnBlanksConverter

C#
public class CollapseOnBlankConverter : IValueConverter
{
    static public bool IsBlank(object value)
    {
        if ((value == null) ||
            ((value is bool) && ((bool)value == false)) ||
            ((value is string) && (((string)value).Length == 0)) ||
            ((value is int) && (((int)value) == 0)) ||
            ((value is double) && (((double)value) == 0)) ||
            ((value is decimal) && (((decimal)value) == (decimal)0)) ||
            ((value is System.DateTime) && 
                ((System.DateTime)value == System.DateTime.MinValue))
            )
            return true;
        return false;
    }
    public object Convert(object value, Type targetType, object parameter, 
           System.Globalization.CultureInfo culture)
    {
        if (CollapseOnBlankConverter.IsBlank(value))
            return System.Windows.Visibility.Collapsed;
        return System.Windows.Visibility.Visible;
    }
    public object ConvertBack(object value, Type targetType, object parameter, 
           System.Globalization.CultureInfo culture)
    {
        throw new NotSupportedException("unexpected Convertback");
    }
}

The static IsBlank method interprets whether the input is blank or not. The value is passed in as an object so it has to determine its type and then perform the relevant comparison. If you are using a type not listed here, then it's a simple matter to add a new clause to the list.

The IValueConverter requires we define conversion going both ways. We never need to convert back, so an exceptions has been added as a sanity check.

CollapseOnBlanksConverter

This converter implements the multi value interface and so loops through the given list.

C#
public class CollapseOnBlanksConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, 
           object parameter, System.Globalization.CultureInfo culture)
    {
        foreach (object v in values)
            if (CollapseOnBlankConverter.IsBlank(v))
                return System.Windows.Visibility.Collapsed;
        return System.Windows.Visibility.Visible;
    }
    public object[] ConvertBack(object value, Type[] targetTypes, 
           object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotSupportedException("unexpected Convertback");
    }
}

CollapseOnBlanksFirstPairOrConverter

This converter is the most complex of the three as it has to treat the first two values differently, so the code is a little more complex. However, it is so much easier to implement complex logic in C# than it is in XAML using Triggers.

C#
public class CollapseOnBlanksFirstPairOrConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, 
                  System.Globalization.CultureInfo culture)
    {
        for (int i = 0; i < values.Length; i++)
        {
            if ((i==0) && (values.Length > 1))
            {
                if ((CollapseOnBlankConverter.IsBlank(values[i])) && 
                    (CollapseOnBlankConverter.IsBlank(values[i+1])))
                    return System.Windows.Visibility.Collapsed;
                i++;
            }
            else if (CollapseOnBlankConverter.IsBlank(values[i]))
                return System.Windows.Visibility.Collapsed;
        }
        return System.Windows.Visibility.Visible;
    }
    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, 
                    System.Globalization.CultureInfo culture)
    {
        throw new NotSupportedException("unexpected Convertback");
    }
}

Conclusion

Hopefully this article will have shown you how WPF can be used in different ways from the standard tutorials, and how a little extra C# coding can go a long way to make your XAML layout a lot easier to develop.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)


Written By
Software Developer
United Kingdom United Kingdom
Tom has been developing software for 20 years and has done a lot of different stuff. Recently this has mostly been C# WPF/Silverlight, ASP.Net and Ruby On Rails.

Blog: http://rightondevelopment.blogspot.com/

Comments and Discussions

 
GeneralGreat Pin
Ravenet9-Oct-09 7:28
Ravenet9-Oct-09 7:28 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.