A pattern for responsive applications in WPF

A pattern for responsive applications in WPF

For UWP or Windows Store apps, the VisualStateManager lets you make state changes at certain widths or breakpoints. Great for building responsive apps. Here is a pattern to do the same in WPF.

What is the VisualStateManager?

The VisualStateManager is great. It is a nice, clean XAML based approach to making presentational changes when an app resizes. Before that I would have to write a lot of c# code to do the same thing, which never felt like a neat approach. If you are unfamiliar with the VisualStateManager here is a good article on the subject.

Make your app look great on any size screen or window

What about WPF?

Whilst doing UI work in WPF, I felt restricted without the VisualStateManager. I really didn't want to go back to writing all this logic in C# again, it felt even more of a hack than before.

Whilst WPF doesn't have the VisualStateManager it does have Style Triggers and Value Converters.

Style Triggers

...triggers are objects that enable you to apply changes when certain conditions (such as when a certain property value becomes true, or when an event occurs) are satisfied.

There are three different types of triggers

  • Property Triggers
  • Event Triggers
  • Data Triggers

Using a Data Trigger we can bind to the width of a parent element. However to convert the width to a Boolean value we have to use a value converter.

Value Converter

A Value Converter

Provides a way to apply custom logic to a binding.

The IsLessThanConverter compares the method arguments value against parameter and returns a boolean.

public class IsLessThanConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value == null || parameter == null)
        {
            return false;
        }

        int firstOperand;
        int secondOperand;

        if (!int.TryParse(value.ToString(), out firstOperand))
        {
            throw new InvalidOperationException("The value could not be converted to an integer");
        }

        if (!int.TryParse(parameter.ToString(), out secondOperand))
        {
            throw new InvalidOperationException("The parameter could not be converted to an integer");
        }

        return firstOperand < secondOperand;
    }

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

<Style.Triggers>
    <DataTrigger Binding="{Binding ElementName=MainContentGrid, Path=ActualWidth}" Value="True">
        <Setter Property="Background" Value="Green" />
    </DataTrigger>
</Style.Triggers>

Using a combination of Style Triggers and a Value Converter, layout changes can be made when the app resized without having to resort to C# code.

The code

Lets take a look at an example.

<Grid x:Name="MainContentGrid">
    <StackPanel Orientation="Horizontal">
        <StackPanel.Style>
            <Style TargetType="StackPanel">
                <Setter Property="HorizontalAlignment" Value="Right"/>
                <Setter Property="Margin" Value="20"/>
                <Style.Triggers>                    
                    <DataTrigger Binding="{Binding Path=ActualWidth, 
                                           ElementName=MainContentGrid, 
                                           Converter={StaticResource IsLessThanConverter}, 
                                           ConverterParameter=1024}"
                                 Value="True">
                        <Setter Property="Margin" Value="0,20,0,0"/>
                    </DataTrigger>
                    <DataTrigger Binding="{Binding Path=ActualWidth, 
                                            ElementName=MainContentGrid, 
                                            Converter={StaticResource IsLessThanConverter}, 
                                            ConverterParameter=1024}"
                                 Value="True">
                        <Setter Property="HorizontalAlignment" Value="Left"/>
                    </DataTrigger>                  
                </Style.Triggers>
            </Style>
        </StackPanel.Style>
        <ToggleButton Margin="0,0,15,0">
            <ToggleButton.Content>
                <TextBlock Text="Cancel"/>
            </ToggleButton.Content>
        </ToggleButton>
        <ToggleButton>
            <ToggleButton.Content>
                <TextBlock Text="Confirm"/>
            </ToggleButton.Content>
        </ToggleButton>
    </StackPanel>
</Grid>

The StackPanel has two Style Triggers for the Margin and HorizontalAlignment properties. These Triggers are bound to the width of the containing Grid. Using the IsLessThanConverterthe property changes are triggered when the width of the Grid is less than 1024.

Watch out for dependency property precedence

You may have noticed I set the default values for Margin and HorizontalAligment in the Style rather than on the element itself.

<StackPanel Orientation="Horizontal">
        <StackPanel.Style>
            <Style TargetType="StackPanel">
                <Setter Property="HorizontalAlignment" Value="Right"/>
                <Setter Property="Margin" Value="20"/>


            </Style>
        </StackPanel.Style>
</StackPanel>

If you set the property value on the element it will always take precedence over those set within the Style. This means that Trigger changes will never show. For more information on dependency property value precedence read this article.

Summary

Here is a XAML based approach allowing you to react to changes in the size of a WPF app. Inspired by the VisualStateManager I believe it gives you a similar approach and feels much nicer than writing C# logic.