For those following the on-going discussion around the future of XAML and specifically the use of XAML in DotNetMaui/Xamarin.Forms, this post is a follow on from two great posts discussing the options that are, or will be, available for Xamarin.Forms developers as we move forward with DotNetMaui:
- Mobile Blazor Bindings – Getting Started + Why You Should Care (Dylan Berry)
- C# Markup versus Mobile Blazor Bindings in .NET MAUI (Vincent Hoogendoorn)
I’ve already discussed why I feel that DotNetMaui will extend Xamarin.Forms to be an inclusive platform for developers wanting to build cross platform applications. Recent additions to Xamarin.Forms, such as C# markup, coupled with the features outlined on the DotNetMaui roadmap, suggest that the path forward will be full of options for developers.
This bring me to the point of this post, which is to discuss XAML both in the context of DotNetMaui/Xamarin.Forms but also in the context of UWP/WinUI/Uno. To do this I’m going to re-visit the scenario discussed by Dylan and Vincent and look at some alternatives that might make XAML a bit more appealing
Xamarin.Forms Visual States
When I initial read through Dylan’s post and reviewed the XAML I was initially shocked at how hard it was to read and in fact it took me a while to realise why it looked so chaotic and hard to follow. It came down to a couple of things:
- Changing XAML attribute values based on data triggers at various points in the XAML
- The lack of Visual States
- Using MultiTrigger to change attribute values based on multiple data values.
Just so that I’m clear, I’m not saying that the code was poorly written; I’m just observing why I found it hard to read. Let me present an alternative XAML for the page and discuss how it makes the code a little more readable.
<?xml version="1.0" encoding="utf-8"?>
<ContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlFlags"
x:Class="XamlFlags.MainPage"
x:Name="Page">
<ContentPage.BindingContext>
<local:MainPageViewModel />
</ContentPage.BindingContext>
<StackLayout
BindableLayout.ItemsSource="{Binding Options}">
<BindableLayout.ItemTemplate>
<DataTemplate>
<Frame
CornerRadius="4"
Padding="0">
<StackLayout
Orientation="Horizontal"
Padding="5"
BackgroundColor="White">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState
x:Name="IsEnabledAndSelected">
<VisualState.StateTriggers>
<StateTrigger
IsActive="{Binding Selected}" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter
Property="BackgroundColor"
Value="DarkBlue" />
<Setter
Property="IsVisible"
Value="False"
TargetName="OptionsSelectedLabel" />
<Setter
Property="Label.TextColor"
Value="White"
TargetName="OptionsValueLabel" />
</VisualState.Setters>
</VisualState>
<VisualState
x:Name="IsNotEnabled">
<VisualState.StateTriggers>
<StateTrigger
IsActive="{Binding IsNotEnabled}" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter
Property="IsEnabled"
Value="false" />
<Setter
Property="BackgroundColor"
Value="DarkGray" />
<Setter
Property="Label.TextColor"
Value="LightGray"
TargetName="OptionsValueLabel" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Button
Text="Select"
Command="{Binding BindingContext.SelectTypeCommand, Source={x:Reference Page}}"
CommandParameter="{Binding .}" />
<Label
Text="✓"
TextColor="White"
x:Name="OptionsSelectedLabel"
FontSize="12"
HorizontalOptions="EndAndExpand"
VerticalOptions="Center"
IsVisible="false" />
<Label
Text="{Binding Value}"
TextColor="Black"
x:Name="OptionsValueLabel"
HorizontalOptions="EndAndExpand"
VerticalOptions="Center" />
</StackLayout>
</Frame>
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>
</ContentPage>
On initial inspection you might be looking at this XAML wondering whether it’s improved the readability. However, if you collapse the VisualStateManager.VisualStateGroups node in the editor, the code immediately looks much more readable, as shown in the following image.
We can clearly make out that the interface is made up of a stack of items with each row being made up off a Button and two Label elements. The default state is for the row to have a White background and for the ✓ Label to be hidden (IsVisible set to false).
Now, let’s look at the contents of the VisualStateManager.VisualStateGroups. In this case it defines a single VisualStateGroup in which I’ve included two VisualState elements. If we consider each row it can be in one of three states:
- Enabled and Not Selected – This is the default state where the item hasn’t been selected (IsEnabled = true, IsSelected = false).
- Enabled and Selected – This is the state where the item has been selected by the user clicking the Select button (IsEnabled = true, IsSelected = true).
- Is Not Enabled – When an item in the list is selected, other items may be set to disabled (IsEnabled = false, IsSelected = false).
Even though there are only two VisualStates defined in the XAML, there are in fact three visual states because there is the default state. Each of the VisualState uses a StateTrigger to determine whether the visual state is active or not. Rather than attempting to data bind to two different properties on the underlying data model, I’ve explicitly created single properties that reflect whether the state is active or not. The default state is both the initial state of each row, as well as the state that is returned to if neither of the defined VisualStates are active (ie the StateTrigger IsActive is set to true).
One of the benefits of using visual states is that you can set multiple properties, on one or more different elements, essentially grouping all the changes required for a particular visual states. This eliminates the need to have data triggers littered through the XAML used to define the layout for the page. In this scenario each of the VisualState elements has multiple Setters defining the background colour, the foreground (text) colour and in the case of the IsEnabledAndSelected state, the visibility of the ✓ Label.
As I pointed out earlier, for me, this makes the XAML easier to read. I know other developers prefer to have the data triggers local to where the attributes being changed are in the XAML. This approach has some advantages, particularly where you only have a small number of changes in a large XAML document.
Before we move on from discussing Xamarin.Forms I would point out that the VisualStateManager is a relatively new addition and specifically the ability to use TargetName to target different elements from a single VisualState. I would encourage revisiting your XAML to see whether using the VisualStateManager will simplify your code. Alternatively, you may want to consider breaking your code up into different controls that will help encapsulate things like visual states whilst reducing the complexity of the XAML on each page. We’ll talk more about using UserControls in the context of UWP shortly but the same concept can be used for Xamarin.Forms as well.
UWP / WindowsUI / Uno
The XAML used by the Universal Windows Platform (ie UWP) provides a couple of alternatives when it comes to defining the desired layout. In this section we’re going to consider three options: Optimised, UserControl and ContainerStyle (Code is available on GitHub)
Optimised for Lines of Code
In this approach the goal was simply to reduce the number of lines of code. I’m not going to spend much time on this because I fundamentally think measuring the quality of a technology based on the number of lines of code is just ridiculous. The following image shows the resulting 41 lines of XAML – of course this is also contingent on my Visual Studio settings where the first attribute is on the same line as the element; other developers have different options set, so will see different numbers of lines.
I will comment on a couple of things:
- Unlike the Xamarin.Forms approach that uses a horizontal StackLayout, this XAML uses a Grid which clearly defines three columns. I prefer this approach as it clearly defines the sizing option for each column. Using a StackLayout with different HorizontalOptions isn’t as clear in my opinion.
- This code uses x:Bind instead of the usual Binding syntax. This allows for code to be generated during compilation that enforces type checking and results in better performance as it reduces the reliance on reflection.
- Rather than using converters this code uses static methods defined on the page to convert property values (in some cases multiple properties) into the required attribute value. In practice I would not recommend doing this as it embeds logic in your XAML layout
UserControl with Visual States
In the previous section we optimised the XAML by interleaving logic into our XAML layout. This is a practice that I actively try to avoid when defining the XAML for a page or control. I try to ensure the XAML I create is as declarative as possible, which means avoiding calling methods as part of binding expressions. I’d even go so far as to say that you should avoid using converters unless they are simple type converters (the most obvious one being for bool to Visibility conversion). I advocate for all logic to be encapsulated in either the ViewModel (if UI relate logic) or in the Model (if business logic), thus making it testable and ensuring a clean separation from the way that it’s presented (i.e. the View).
Let’s look at how we can keep our XAML clean of logic by following a similar approach to what we did earlier for Xamarin.Forms. What’s interesting about UWP is that if you attempt to follow the same strategy of defining visual states within the DataTemplate you’ll find that it doesn’t work. Instead you either need to define the visual states within a UserControl, or you have to apply the visual states in the ItemContainerStyle.
We’ll start with defining a UserControl which will include the layout for each item in the list, along with the different visual states. Rather than just show you the code, let’s jump into Blend and use the visual designer to build the desired layout. Rather than boring you with creating the project and adding things like the MainViewModel, I’m just going to focus on creating the UserControl.
From Solution Explorer, right-click on the project or a sub-folder and select Add, New Item.
From the Add New Item dialog, select User Control, give it a name (eg OptionItemControl) and click Add. This should give you a new design surface that we can start adding controls to.
Using the Assets tool window, double-click on the Button and then twice on the TextBlock. Alternatively you can drag these controls onto the design surface.
If you look at the XAML you’ll notice that Blend has added some default attributes to each of the controls that you added. Normally my recommendation would be to tidy up whatever XAML Blend creates, as you go, to ensure your XAML remains clean and does exactly what you want. If you’re not that familiar with XAML, reviewing the XAML that Blend generates is a great way to learn about features that you might not be aware of.
If we look at the design surface we can see that the three controls we added are incorrectly located on the design surface – we’re after a single row with three cells that should hold the “select” Button, a check mark TextBlock and the option value TextBlock.
Let’s go ahead and create the cells in the Grid by defining three columns. We can do this using the design by hovering the mouse near the top of the design surface. The cursor should change to include a small + sign and if you click with the mouse a new column break will be added. You can then use the designer to specify how the column widths are defined. In this case we want the first column to be Auto and the other two columns to be * (just *, so you may have remove any numeric value added by Blend eg 101* should be changed to just *)
Next we want to reposition the two TextBlock into the appropriate column. You can do this by simply dragging them using the mouse. As you drag and hover over each cell in the Grid you’ll see alignment and positioning lines appear in red.
After positioning the TextBlock into the correct column you will want to tidy up the XAML – Blend has an annoying habit of adding Margins and other layout attributes that are typically unnecessary. You can clean up these excess attributes by right-clicking the control and selecting Layout, Reset All.
The next thing to do is to replace the default Text value for the TextBlock from “TextBlock” to something more meaningful. Rather than having to locate the Text property in the properties tool window, you can simply double-click the TextBlock and type directly on the designer. You can do the same thing with the Button control to change the Content to “Select”.
According to the design we’re aiming to achieve we need to change the default (i.e. Enabled but not Selected) Background to White. Use the colour picker in the Properties tool window to do this. This of course will override the theming and thus break support for dark and light themes, but that’s a topic for another day.
Unfortunately setting the Background to White means we can no longer see the three controls, since their default text colour is currently also White. This can be fixed easily by selecting all three controls in the Objects and Timelines tool window.
From the Properties tool window, update the Foreground colour to Black.
You’ll also need to adjust the BorderBrush on the Button. After doing this the layout should be similar to the following.
And the XAML for this is quite tidy.
What’s missing are the visual states for when the option is selected and when it’s disabled. We’ll add two VisualStates to the UserControl via the States window. Normally I advocate for creating a third VisualState that represents the default state – this is so that in code you can use the VisualStateManager GoToState method to return to the default layout. However, we’re going to be using state triggers to transition between states, so a third VisualState is not required – when none of the state triggers are set to active, the UserControl will return to the default state.
By clicking on a VisualState in the States tool window we can put Blend into state editing mode. This is highlighted by both the red dot alongside the VisualState, as well as the red border around the design surface. Any changes you make whilst this highlighting is visible will be recorded against the corresponding VisualState.
I’m not going to go through setting all the properties for each VisualState but essentially we need to update the Background colour on the Grid, the Foreground colour on the TextBlock and Button, and the Visibility on the checkmark TextBlock. For the checkmark TextBlock, you’ll need to switch the default Visibility to Collapsed – we had it visible whilst designing the layout but it’s default state is actually to be hidden (i.e. Collapsed).
As part of defining the VisualStates you may have noticed that the controls in the Objects and Timeline tool window were all given default names like grid, button, textBlock and textBlock1. I would encourage you to give these more meaningful names by double-clicking on them in the Objects and Timeline tool window and typing a new name.
Currently we have completed the design of the UserControl, including the three different states it can be in. However, we haven’t wired it up to any data. If we were to use this UserControl as it is, it would simply show a list of “Option 1”. So that we can wire up some data, let’s add an instance of the OptionViewModel as a design time DataContext for the UserControl (note the use of the d: prefix to indicate design time only).
Select the TextBlock that should show the OptionViewModel Value property. Click on the small square to the right of the Text property in the Properties tool window and select Create Data Binding.
Since we’ve set up an instance of the OptionViewModel as a design time DataContext, Blend is able to offer suggestions for the Path attribute. Select the Value property and click Ok.
At this point you’ll be wondering why the TextBlock that you just setup data binding for as disappeared from the designer. Well, the good news is that the TextBlock is still there, it just has zero width because the Value property on the OptionViewModel is returning null. This is easily fixed by specifying the Value property on the OptionViewModel specified in the design time DataContext.
Ok, we’re almost there. We have our layout, our Visual States and our data. We still need to be able to trigger then changing of states. For this we’re going to use a state trigger. Click the “Edit Adaptive Triggers” button next to the VisualState.
From the Dropdown at the bottom of the screen select <Other Type…>
In our project we have a trigger called DualBooleanDataTrigger which will allow us to trigger a VisualState based on the value of two different bool properties.
Unfortunately this is about as far as the data trigger design experience can take us. Luckily the XAML for the DualBooleanDataTrigger isn’t that complex. As you can see from this image, we need to data bind the DataValue and SecondDataValue attributes to the IsEnabled and IsSelected properties on the OptionViewModel and then we need to specify the value for each of these properties that we want the VisualState to be active for. In this case we want both the IsEnabled and IsSelected properties to be True for this state to be active.
The last thing we need to do is to wire up the Button to a method that will set the OptionViewModel to be selected. For this, we’re going to use x:Bind so that we can bind directly to a method on the OptionViewModel. In order to do this, we need to expose the the OptionViewModel as a property on the UserControl.
In the MainPage (i.e. where we’re defining the ItemsControl) we need to include an instance of the OptionItemControl in the DataTemplate. In order for x:Bind to work we need to make sure we set the ViewModel to be the current DataContext of the DataTemplate (i.e. binding path of . which can be omitted in this scenario).
We then need to specify the Click attribute on the Button.
After completing this we’re done! But let’s take a look at what’s been generated. Well, the good news is that we’ve created the entire user experience without having to write much XAML at all. The bad news is that Blend does a terrible job of generating optimized XAML. Take for example the following snippet where you can see that each Setter is spread over 5 lines.
In contrast, when I tidy these up, each Setter takes only 2 lines and that’s only because I have Visual Studio and Blend set to put attributes on new lines.
As you can see from this walk through, you can use a separate UserControl to easily design the layout for the three states required in this design.
ListView ItemsContainerStyle
The third option I wanted to briefly discuss (because there’s plenty of room for an entire discussion on this topic) is to override the ItemsContainerStyle for a ListView in order to define the VisualStates for the items in the list. Let’s back up for a second and explain a few things.
The scenario that we’re addressing is a common enough scenario where items in a list are selected. In this case the individual item tracks whether it’s selected (and in fact whether it’s enabled). This approach is actually at odds with the way a number of controls work. Take for example the ListView or GridView which are used to represent a list, or grid, of selectable items. Theses controls have built in styles that represent how each ListViewItem will look in various states. These states include (not an exhaustive list) Selected, Pressed, Enabled and Disabled. Further more, these control separate the appearance of each item (specified using the ItemTemplate) from the behaviour when the user interacts with the items (specified in the ControlTemplate used to define the ListViewItem Template nested in the ItemsContainerStyle). Both the ItemTemplate and ItemsContainerStyle are editable using the visual designer, allowing you to customise the appearance of the item and how it changes when the item is selected or enabled/disabled.
For example in Blend we can right-click on a ListView and select Edit Additional Templates, Edit Generated Item Container (ItemContainerStyle) and then either Edit Current, Edit Copy or Create Empty. I would suggest starting by selecting Edit Copy, which will give you a default style that you can customise, rather than starting from scratch.
In the following image you can see how I’ve defined different VisualStates for the ListViewItem to control the appearance for the three states the item can be in. You can also see all the other states that are available by default on the ListViewItem.
At this point you might be asking why, since the ListViewItem already has a Selected and a Disabled state, don’t I just use these. Well, the issue we have is that the Background and Foreground properties have three different colours based on the combination of two different properties (IsEnabled and IsSelected). This means we need to define states that are in the same group that respond to changes in both these properties. This is not how the built in VisualStates are defined. The Selected and Disabled states are in different VisualStateGroups – if we were to use these to control the same properties, we’ll end up with random and chaotic changes in the appearance.
VisualStates in different VisualStateGroups should not control the same property.
This is an important rule and one that’s easily forgotten or broken. If you ever see unusual behaviour relating to visual states, make sure you check this rule.
I’m not going to delve further into how to use the ItemsContainerStyle in this post but feel free to check out the source code. You’ll notice that this is very verbose coming in at around 130 lines, and this is using a much simplified ItemsContainerStyle. Unfortunately due to the complexity of the built in styles, the ItemsContainerStyle is a large style, mainly because the Template for the ListViewItem is so large. Don’t let this put you off considering using this approach because it does mean that you can reuse the style across various lists that may have different content.
Summary
Wow, sorry this ended up being quite a long post but hopefully you’ll find it useful and get some insight on the various XAML based options available to you. Don’t forget that all the options presented for UWP are able to be taken cross platform using the Uno Platform.
In summary, yes, XAML is verbose but it does come with options. If you spend time maintaining your XAML you’ll find that it is easy to understand and you can leverage the designer support in Blend and Visual Studio along the way.
I don’t believe that looking at the number of lines of XAML (or C# code) is productive. What is useful is to look at the options that are available and the easy with which you can achieve the desired outcome.
Thanks