Preface to revision
This post was updated significantly on February 13, 2016 as I realized I had once again not adhered to the advice my late Math teacher, Mr. Rang, gave my class somewhere in the 80’s – “do not try to be clever”. My previous attempt at doing this used a clever mechanism indeed to find and replace styles based upon the style names – names which you had to supply as a string. This limited the use to local resources, and also gave some stability issues. It took me some two weeks to realize I don’t need code for looking up styles by name - XAML has a perfectly built-in mechanism for that. It’s been there for ages. It’s called StaticResource. Yeah. Indeed. For current status – see image on the right. Anyway, I have adapted my behavior, sample code, and now this post.
Intro
As I was in the process of porting my app Map Mania to the Universal Windows Platform, I of course extensively used Adaptive Triggers. Now that works pretty darn well, unless you have a whole lot of elements that need to be changed. The credits page of my app looks like this
that is to say, that is what is looks on a narrow screen. When I have a wider screen, like a Surface Pro, I want it to look like this:
The font size of all the texts need to increase. This is rather straightforward, but also a rather cumbersome experience when there are a lot of elements to change. To get this done with the normal Adaptive Triggers in a VisualStateGroup, you have to:
- Give all elements an x:Name
- Refer to them by name into your trigger’s setter
- For every element you have set the change to the font size.
This very quickly becomes a copy & paste experience, especially if you have a lot of elements and states. And of course, when you add a new element (adding new credits, for example translators), you might forget to add it to the VSM, so you have to go back again… Don’t get me wrong, the Adaptive Trigger is an awesome idea, but in some situations it’s a lot of work to get things done. If you have been reading this blog for some time you already know what is coming: my castor oil for all XAML related challenges – a behavior.
Introducing StyleReplaceBehavior
I have created a simple behavior that, in a nutshell, does the following:
- You supply the StyleReplaceBehavior with the names of styles you want to have ‘observed’. For instance “MySmallStyle”, “MyMediumStyle”, “MyBigStyle”. This is the property ObservedStyles.
- From the Visual State Manager, you set the behavior’s CurrentState property to the style you want to be activated. For instance, “MyMediumStyle”
- The StyleReplaceBehavior traverses the whole visual tree from it’s location and finds every element that has an observed style that is not the currently wanted style, and changes it’s style to the wanted style. So following with the example I just used, if finds everything that is “MySmallStyle” or “MyBigStyle” and changes it to “MyMediumStyle”.
Usage example
As usual, I have supplied a demo project, but I will explain in more detail how it works. About halfway down in the MainPage.Xaml you will find the XAML that defines the credits grid:
<Grid x:Name="ContentPanel" Height="Auto" VerticalAlignment="Top" > <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <HyperlinkButton Content="Laurent Bugnion" Tag="http://www.twitter.com/lbugnion" Style="{StaticResource CreditHyperlinkButtonStyle}" Click="HyperlinkButtonClick" /> <TextBlock Text="MVVMLight" Grid.Row="0" Grid.Column="1 " Style="{StaticResource CreditItemStyle}" /> <HyperlinkButton Content="Mike Talbot" Tag="http://whydoidoit.com/" Grid.Row="1" Style="{StaticResource CreditHyperlinkButtonStyle}" Click="HyperlinkButtonClick" /> <TextBlock Text="SilverlightSerializer" Grid.Row="1" Grid.Column="1" Style="{StaticResource CreditItemStyle}" /> <HyperlinkButton Content="Matteo Pagani" Tag="http://www.twitter.com/qmatteoq" Grid.Row="2" Style="{StaticResource CreditHyperlinkButtonStyle}" Click="HyperlinkButtonClick" /> <TextBlock Text="General advice, explanation Template10" Grid.Row="2" Grid.Column="1" Style="{StaticResource CreditItemStyle}" /> <HyperlinkButton Content="Morten Nielsen" Tag="http://www.twitter.com/dotmorten" Grid.Row="3" Style="{StaticResource CreditHyperlinkButtonStyle}" Click="HyperlinkButtonClick" /> <TextBlock Text="WindowsStateTriggers" Grid.Row="3" Grid.Column="1" Style="{StaticResource CreditItemStyle}" /> <HyperlinkButton Content="Microsoft" Tag="http://www.microsoft.com/" Grid.Row="4" Style="{StaticResource CreditHyperlinkButtonStyle}" Click="HyperlinkButtonClick" /> <TextBlock Text="Windows, Visual Studio & Template10" Grid.Row="4" Grid.Column="1" Style="{StaticResource CreditItemStyle}" /> </Grid>
This may look impressive, but I can assure you it is not – it’s only a bunch of texts and buttons, and the only interesting thing is the usage of CreditHyperlinkButtonStyle and CreditItemStyle. A little but further up, in the page’s resources, you will see the actual styles that are used getting defined:
<Style x:Key="PageTextBaseStyle" TargetType="TextBlock" BasedOn="{StaticResource BaseTextBlockStyle}"> <Setter Property="FontSize" Value="15"/> </Style> <Style x:Key="CreditItemStyle" TargetType="TextBlock" BasedOn="{StaticResource PageTextBaseStyle}" > <Setter Property="FontStyle" Value="Italic"/> <Setter Property="VerticalAlignment" Value="Center"/> <Setter Property="TextWrapping" Value="Wrap"/> <Setter Property="Margin" Value="20,5,0,0"/> </Style> <Style x:Key="CreditItemStyleBig" TargetType="TextBlock" BasedOn="{StaticResource CreditItemStyle}"> <Setter Property="FontSize" Value="25"/> </Style> <Style x:Key="HyperlinkTextStyle" TargetType="TextBlock" BasedOn="{StaticResource PageTextBaseStyle}" > </Style> <Style x:Key="HyperlinkTextStyleBig" TargetType="TextBlock" BasedOn="{StaticResource PageTextBaseStyle}" > <Setter Property="FontSize" Value="25"/> </Style>
And basically the only difference between the normal and the –Big styles is that their font size is not 15 but 25.
The behavior – which needs to be attached to the page itself - is used as follows:
<interactivity:Interaction.Behaviors> <behaviors:StyleReplaceBehavior x:Name="CreditItemStyler"> <behaviors:StyleReplaceBehavior.ObservedStyles> <behaviors:ObservedStyle Style="{StaticResource CreditItemStyle}"/> <behaviors:ObservedStyle Style="{StaticResource CreditItemStyleBig}"/> </behaviors:StyleReplaceBehavior.ObservedStyles> </behaviors:StyleReplaceBehavior> <behaviors:StyleReplaceBehavior x:Name="HyperlinkTextStyler"> <behaviors:StyleReplaceBehavior.ObservedStyles> <behaviors:ObservedStyle Style="{StaticResource HyperlinkTextStyle}"/> <behaviors:ObservedStyle Style="{StaticResource HyperlinkTextStyleBig}"/> </behaviors:StyleReplaceBehavior.ObservedStyles> </behaviors:StyleReplaceBehavior> </interactivity:Interaction.Behaviors
In fact, you see that there are two, each for every style pair that needs to be changed. Also note the behaviors have names. This we need to be able to address them from the Visual State Manager:
<VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="WindowStates"> <VisualState x:Name="NarrowState"> <VisualState.StateTriggers> <AdaptiveTrigger MinWindowWidth="0"></AdaptiveTrigger> </VisualState.StateTriggers> <VisualState.Setters> <Setter Target="CreditItemStyler.CurrentStyle" Value="{StaticResource CreditItemStyle}"/> <Setter Target="HyperlinkTextStyler.CurrentStyle" Value="{StaticResource HyperlinkTextStyle}"/> </VisualState.Setters> </VisualState> <VisualState x:Name="WideState"> <VisualState.StateTriggers> <AdaptiveTrigger MinWindowWidth="700"></AdaptiveTrigger> </VisualState.StateTriggers> <VisualState.Setters> <Setter Target="CreditItemStyler.CurrentStyle" Value="{StaticResource CreditItemStyleBig}"/> <Setter Target="HyperlinkTextStyler.CurrentStyle" Value="{StaticResource HyperlinkTextStyleBig}"/> </VisualState.Setters> </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups>
Which is, apart from that it is setting behavior properties in stead of element properties, as bog standard as you can think of. And that is all. An Adaptive Trigger and some settings.
Caveats and limitations
The behavior is not very forgiving. If you use a style that is not applicable to the element you want to change, it will probably hard crash on you. It does the job, but it’s not intended to catch all your mistakes. Intentionally, by the way, because it might become silently defunct if you change style names and you forget to edit the behavior’s use. The revised version, though, is no longer limited to local resources. It can use any resource that can be identified by StaticResource.
A little peek behind the curtains
The behavior itself pretty simple – two dependency properties, ObservedStyles and CurrentStyle, but those are not interesting in itself. There is one little method that actually does all the work when the behavior initializes, or CurrentStyle changes:
private void ReplaceStyles(Style newStyle) { if (!Windows.ApplicationModel.DesignMode.DesignModeEnabled) { // Find all other styles observed by this behavior var otherStyles = ObservedStyles.Where( p => p.Style != newStyle).Select(p=> p.Style); // Find all the elements having the other styles var elementsToStyle = AssociatedObject.GetVisualDescendents().Where( p => otherStyles.Contains(p.Style)); // Style those with the new style foreach (var elementToStyle in elementsToStyle) { elementToStyle.Style = newStyle; } } }
Four lines of code – that is basically all there is to it. And since nearly every line has comments to describe it, I will refrain from further detailing, except for one thing – GetVisualDescendents is not a normal method, but an extension method sitting in WpWinNl – the one and only reason I attached my package to it.
Conclusion
I realize this is huge overkill if you have only two or three elements to change, but when it becomes a lot, this may help you to you make your UWP apps even more awesome pretty easily. For me it saved a lot of work. Once again, the link to the demo project, where you can see everything running.
An additional conclusion is that Mr. Rang – may he rest in peace – once again was right. Don’t try to be clever. Simply using the tools that are already there usually works better.