Restyling Controls in an Uno (Windows UI) Application

In my previous post, Multi-Platform PixelPerfect UI with Windows UI and the Uno Platform, I demonstrated how applications built using Project Reunion (i.e. Windows UI), coupled with the Uno Platform, could deliver an identical user experience across a variety of platforms. In this post I’m going to walk through changing the style of one of the controls. This process isn’t specific to this one control, it’s the standard way that you can override the style, or template, for any of the Windows UI controls.

I’ve already shared the outcome of what I’m looking to achieve – instead of the typical accent coloured border around the TextBox when it has focus, I wanted to apply the Uno logo colours along each edge. The result:

As you can see from this image, I haven’t modified either the Normal or Disabled states (top left and right respectively). However, the PointerOver and Focused states have been updated to use the colours of the Uno logo – the only difference is that the Opacity of the border in the PointerOver state is set to 50%.

So, how do we go about doing this? Well for every Windows UI control, there is a default template. To get started, we want to import this default template so that we can make incremental changes, rather than having to build everything from the ground up. This is an important step as a lot of the Windows UI controls have quite complex templates that accommodate a range of different control states. Starting from scratch can result in a very lean template but may also miss some visual affordances for some of the visual states. For this reason, I typically import the default template and then make the smallest adjustments necessary in order to achieve the outcome I desire.

To find the default template for the TextBox, you need to open the generic.xaml file that’s included as part of the ProjectReunion NuGet package. I’ve posted about this previously, Colors, Styles and Templates in Windows UI (WinUI), but the location for WinUI3 needs to be updated since it has been included in the ProjectReunion namespace. Eg

C:\Users\[user]\.nuget\packages\microsoft.projectreunion.winui\0.5.0\lib\net5.0-windows10.0.18362.0\Microsoft.WinUI\Themes\generic.xaml 

The generic.xaml file is quite large, so the easiest thing to do is to search for TargetType="TextBox", which will locate the Style for the TextBox. You can either copy the entire Style, or copy the ControlTemplate element (i.e the Setter.Value of the Template property). In this case, we’re only interested in modifying the template, so we’ll copy the entire ControlTemplate into the Page.Resources for our application.

<Page.Resources>
    <SolidColorBrush x:Key="UnoBlue"
                        Color="#FF159BFF" />
    <SolidColorBrush x:Key="UnoRed"
                        Color="#FFF85977" />
    <SolidColorBrush x:Key="UnoPurple"
                        Color="#FF7A67F8" />
    <SolidColorBrush x:Key="UnoGreen"
                        Color="#FF67E5AD" />

    <ControlTemplate x:Key="UnoTextBoxTemplate"
                        TargetType="TextBox">
	<!-- Details omitted for brevity -->
    </ControlTemplate>
</Page.Resources>

You’ll notice I’ve also added four SolidColorBrush resources that represent the four colours from the Uno logo.

The next thing we’re going to do is to locate the single Border element in the template with the name BorderElement – if you look through the various visual states you can see that the BorderBrush and Background of this element change to highlight what state the TextBox is in. We’re going to replace this single Border with four Border elements nested in a Grid. This will give us the ability to modify the colour of each Border independently.

<Grid x:Name="BorderElement"
        Grid.Row="1"
        Grid.Column="1"
        Grid.RowSpan="1"
        Grid.ColumnSpan="2"
        Background="{TemplateBinding Background}"
        CornerRadius="{TemplateBinding CornerRadius}"
        Control.IsTemplateFocusTarget="True"
        MinWidth="{ThemeResource TextControlThemeMinWidth}"
        MinHeight="{ThemeResource TextControlThemeMinHeight}">
    <Border x:Name="BorderLeft"
            BorderBrush="{TemplateBinding BorderBrush}"
            BorderThickness="2,0,0,0"
            Margin="0,0,0,2"
            CornerRadius="{TemplateBinding CornerRadius}" />
    <Border x:Name="BorderTop"
            BorderBrush="{TemplateBinding BorderBrush}"
            BorderThickness="0,2,0,0"
            Margin="2,0,0,0" />
    <Border x:Name="BorderRight"
            BorderBrush="{TemplateBinding BorderBrush}"
            BorderThickness="0,0,2,0"
            Margin="0,2,0,0" />
    <Border x:Name="BorderBottom"
            BorderBrush="{TemplateBinding BorderBrush}"
            BorderThickness="0,0,0,2"
            Margin="0,0,2,0" />
</Grid>

You’ll notice that I’ve assigned the BorderElement name to the parent Grid so that any changes to the Background will still be correctly applied. Each of the child Border elements are named according to which side of the TextBox they are located on. One thing I will point out is that I’ve hard coded the BorderThickness and Margin based on the default BorderThickness of 2 – I’d recommend extracting these into appropriate resources if you’re going to be using this in an actual application.

The last thing to do is to modify each of the visual states of the TextBox to apply the Uno colours to the four Border elements instead of the default theme colours to the BorderElement. I simply did a search for each reference to the BorderElement and adjust the visual states appropriately. For example, the PointerOver state changes from:

<VisualState x:Name="PointerOver">
    <Storyboard>
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderElement"
                                        Storyboard.TargetProperty="BorderBrush">
            <DiscreteObjectKeyFrame KeyTime="0"
                                    Value="{ThemeResource TextControlBorderBrushPointerOver}" />
        </ObjectAnimationUsingKeyFrames>
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderElement"
                                        Storyboard.TargetProperty="Background">
            <DiscreteObjectKeyFrame KeyTime="0"
                                    Value="{ThemeResource TextControlBackgroundPointerOver}" />
        </ObjectAnimationUsingKeyFrames>
        <!-- Changes to other elements -->
</VisualState>

to:

<VisualState x:Name="PointerOver">
    <Storyboard>
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderLeft"
                                        Storyboard.TargetProperty="BorderBrush">
            <DiscreteObjectKeyFrame KeyTime="0"
                                    Value="{StaticResource UnoBlue}" />
        </ObjectAnimationUsingKeyFrames>
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderTop"
                                        Storyboard.TargetProperty="BorderBrush">
            <DiscreteObjectKeyFrame KeyTime="0"
                                    Value="{StaticResource UnoRed}" />
        </ObjectAnimationUsingKeyFrames>
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderRight"
                                        Storyboard.TargetProperty="BorderBrush">
            <DiscreteObjectKeyFrame KeyTime="0"
                                    Value="{StaticResource UnoPurple}" />
        </ObjectAnimationUsingKeyFrames>
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderBottom"
                                        Storyboard.TargetProperty="BorderBrush">
            <DiscreteObjectKeyFrame KeyTime="0"
                                    Value="{StaticResource UnoGreen}" />
        </ObjectAnimationUsingKeyFrames>
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderElement"
                                        Storyboard.TargetProperty="Opacity">
            <DiscreteObjectKeyFrame KeyTime="0"
                                    Value="0.5" />
        </ObjectAnimationUsingKeyFrames>
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderElement"
                                        Storyboard.TargetProperty="Background">
            <DiscreteObjectKeyFrame KeyTime="0"
                                    Value="{ThemeResource TextControlBackgroundPointerOver}" />
        </ObjectAnimationUsingKeyFrames>
        <!-- Changes to other elements -->
    </Storyboard>
</VisualState>

Updated: In the original post I forgot to point out that you need to set the Template property on the TextBox, as shown in the following XAML. If you want to apply this to all TextBox elements you need to create an implicit Style for the TextBox that sets the Template property.

<TextBox Margin="20,0"
            Text="TextBox"
            Template="{StaticResource UnoTextBoxTemplate}"
            HorizontalAlignment="Stretch" />

Whilst changing the style of a control like this requires importing a lot of XAML, the reality is that you can make changes incredibly easily as the visual appearance of each state of the control is declared in XAML. For anyone who simply doesn’t get the value proposition of XAML, I welcome anyone to point out another framework/technology that lets you override the template of a control independently of the behaviour of the control (i.e. the concept of Lookless Controls which seems to have been lost to the internet archives of Silverlight and WPF).

Note: You could in theory do all of this in C# as it’s actually the template model that sits behind the UWP/WinUI XAML implementation that’s important but I actually prefer the separation of XAML and C# (codebehind/viewmodel).

2 thoughts on “Restyling Controls in an Uno (Windows UI) Application”

Leave a comment