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 IsLessThanConverter
the 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.