Tuesday, March 11, 2014

Fun with Expanders

This post is for WPF version 4.0

I have a WPF page with a lot of data on it so to make the users' lives easier we broke the page vertically into five areas, each in its own expander. The user can expand and collapse sections of the page depending on their current task. The technique is fairly simple. Let's start a new WPF project called "Expanders". Change MainWindow to use the following XAML.

<Window x:Class="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525">
    <StackPanel>
        <Expander Name="Expander1" Header="Expander 1">
            <TextBlock Text="Contents of Expander 1"></TextBlock>
        </Expander>
        <Expander Name="Expander2" Header="Expander 2">
            <TextBlock Text="Contents of Expander 2"></TextBlock>
        </Expander>
        <Expander Name="Expander3" Header="Expander 3">
            <TextBlock Text="Contents of Expander 3"></TextBlock>
        </Expander>
        <Expander Name="Expander4" Header="Expander 4">
            <TextBlock Text="Contents of Expander 4"></TextBlock>
        </Expander>
        <Expander Name="Expander5" Header="Expander 5">
            <TextBlock Text="Contents of Expander 5"></TextBlock>
        </Expander>
    </StackPanel>
</Window>
If you cut and paste that into your MainWindow you'll see the effect we're trying to achieve. Nothing too ground breaking there. But when you actually put content into the expanders some of them will scroll off the bottom of the page. Also, some users like to simply expand everything when the page first comes up and it's awkward to have to scroll down to find some of the expanders. We want to add a row of expanders across the top of the page that are tied into the expanders we just created. We can easily tie the IsExpanded property of the new expanders to the IsExpanded property of the existing expanders. When we toggle one, the other gets toggled too. The resulting XAML looks like this...

<Window x:Class="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525">
    <StackPanel>
        <StackPanel Orientation="Horizontal">
            <Expander Name="TopExpander1" Header="Expander 1" IsExpanded="{Binding ElementName=Expander1, Path=IsExpanded}" Margin="0,0,20,20"/>
            <Expander Name="TopExpander2" Header="Expander 2" IsExpanded="{Binding ElementName=Expander2, Path=IsExpanded}" Margin="0,0,20,20"/>
            <Expander Name="TopExpander3" Header="Expander 3" IsExpanded="{Binding ElementName=Expander3, Path=IsExpanded}" Margin="0,0,20,20"/>
            <Expander Name="TopExpander4" Header="Expander 4" IsExpanded="{Binding ElementName=Expander4, Path=IsExpanded}" Margin="0,0,20,20"/>
            <Expander Name="TopExpander5" Header="Expander 5" IsExpanded="{Binding ElementName=Expander5, Path=IsExpanded}" Margin="0,0,20,20"/>
        </StackPanel>
        <Expander Name="Expander1" Header="Expander 1">
            <TextBlock Text="Contents of Expander 1"></TextBlock>
        </Expander>
        <Expander Name="Expander2" Header="Expander 2">
            <TextBlock Text="Contents of Expander 2"></TextBlock>
        </Expander>
        <Expander Name="Expander3" Header="Expander 3">
            <TextBlock Text="Contents of Expander 3"></TextBlock>
        </Expander>
        <Expander Name="Expander4" Header="Expander 4">
            <TextBlock Text="Contents of Expander 4"></TextBlock>
        </Expander>
        <Expander Name="Expander5" Header="Expander 5">
            <TextBlock Text="Contents of Expander 5"></TextBlock>
        </Expander>
    </StackPanel>
</Window>


But clicking all those expanders is so tedious! The users want a single "All" expander. If any expanders are closed clicking All will expand all of them. If all expanders are open then clicking All will close all of them. We can do this with a simple multi value converter. The IsExpanded property of the All expander is tied to the IsExpanded properties of all the other expanders via the converter. The converter looks like this...



VB.Net
Public Class AllExpanderConverter Implements IMultiValueConverter Public Function Convert(values() As Object, targetType As System.Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IMultiValueConverter.Convert ' Should be 5 IsExpanded booleans ' If any are (collapsed) false we return false (so we can expand them all) ' but if all are expanded (true) we return true (expanded) so we can collapse them all Dim bExpand As Boolean = True For i As Integer = 0 To values.Length - 1 If Not values(i) Then bExpand = False Next Return bExpand End Function Public Function ConvertBack(value As Object, targetTypes() As System.Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object() Implements System.Windows.Data.IMultiValueConverter.ConvertBack ' When the All expander is toggled, set all other expanders to the same value Dim IsExpanded(5) As Object For i As Integer = 0 To 4 IsExpanded(i) = value Next Return IsExpanded End Function End Class
C#.Net
public class AllExpanderConverter : IMultiValueConverter { public object Convert(object[] values, System.Type targetType, object parameter, System.Globalization.CultureInfo culture) { // Should be 5 IsExpanded booleans // If any are (collapsed) false we return false (so we can expand them all) // but if all are expanded (true) we return true (expanded) so we can collapse them all bool bExpand = true; for (int i = 0; i <= values.Length - 1; i++) { if (!(bool)values[i]) bExpand = false; } return bExpand; } public object[] ConvertBack(object value, System.Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture) { // When the All expander is toggled, set all other expanders to the same value object[] IsExpanded = new object[5]; for (int i = 0; i <= 4; i++) { IsExpanded[i] = value; } return IsExpanded; } }


To reference this converter we need to add an xmlns attribute to the Window...

    xmlns:local="clr-namespace:Expanders"

We also need to add a resource to the MainWindow...

    <Window.Resources>
        <local:AllExpanderConverter x:Key="AllExpanderConverter"/>
    </Window.Resources>

And the All expander is inserted at the beginning of the horizontal stack panel with this XAML...

            <Expander Name="TopAllExpander"  Header="All" Margin="0,0,40,5" >
                <Expander.IsExpanded>
                    <MultiBinding Converter="{StaticResource AllExpanderConverter}" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" >
                        <Binding ElementName="Expander1" Path="IsExpanded"/>
                        <Binding ElementName="Expander2" Path="IsExpanded"/>
                        <Binding ElementName="Expander3" Path="IsExpanded"/>
                        <Binding ElementName="Expander4" Path="IsExpanded"/>
                        <Binding ElementName="Expander5" Path="IsExpanded"/>
                    </MultiBinding>
                </Expander.IsExpanded>
            </Expander>

So with some XAML, some binding, and a simple converter we have some nice functionality.

No comments:

Post a Comment