In this post we’re going to cover creating a custom control that uses a control template to define how it looks, aka a Templated Control. The principles of templated, or lookless, controls have been adopted by most of the XAML based technologies but for the purpose of this post we’re going to start by building for Windows (ie UWP) and then we’re going to leverage Uno to deliver the same control across iOS, Android and even the web using Web Assembly (WASM).
Full source code available on GitHub
Disclaimer: The purpose of this post is to walk through the process of creating a Templated Control. To do this we’re going to create a multi-switch control (i.e. a switch that has multiple positions). However, I haven’t attempted to win any design awards with this control. In fact the entire point of a Templated Control is that it’s possible to restyle the control and add animation etc without changing the basic functionality of the control.
Getting Started – Uno Project Templates
To get started a bit of house keeping – let’s make sure we have our project setup so that we can build a Templated Control in its own library (so we can reuse it) and that we have a set of head projects where we can test out our control. Since we’re going to use Uno to take our control cross platform, we’ll use the Uno project templates to get us started.
We’ll start by creating a new project based on the Cross-Platform App (Uno Platform). If you don’t have the Uno project templates installed you can grab the Uno Platform Solution Templates Visual Studio extension from the marketplace.
Set some basic project information – in this case our head projects are just for the purpose of testing our Templated Control so we’ve named it XAMLControlSample.
Once you’ve completed creating the new project you should have a solution with four head projects (iOS, Android, UWP and WASM) as well as a shared project. The XAML for the MainPage is in the shared project, which is where we’ll be adding an instance of our Templated Control to test it out after we’ve created it.
Speaking of which, we need to create a library for our Templated Control to reside in. We’ll add another project, this time based on the Cross-Platform Library (Uno Platform) project template. If you’re not interested in taking your Templated Control cross platform (i.e. you’re just building for UWP) you can simply create a class library based on the Class Library (Universal Windows) project template. The big difference with the Uno template is that it creates a project that is setup with multi-targeting, meaning that it will create a library that will have an iOS, Android, Windows and WASM binaries.
We’ll give our class library a name, in this case MyCustomControls.
The next step is to create our Templated Control. Unfortunately due to the limited support for multi-targeting within Visual Studio, if you attempt to add a new item directly to the class library, you won’t see any of the Windows Universal item templates. Instead what we need to do is to create the Template Control in the UWP head project and move the relevant files across to the class library. Right-click on the UWP head project and select Add, New Item. In the Add New Item dialog, select the Templated Control item template and give the control a name, in this case MultiSwitchControl.
After adding the Templated Control you should see two files added to the UWP head project: Generic.xaml (in the Themes folder) and MultiSwitchControl.cs (you Templated Control). Note that there’s no XAML file for the Templated Control (i.e. there’s no MultiSwitchControl.xaml), which you would get if you were creating a UserControl. This is because the XAML that defines how the Templated Control looks is all contained in the Style and the associated ControlTemplate.
The final piece of setup is just to move these two files, including the Themes folder, into the class library. After moving the files, you should make sure that you update the namespace of your Templated Control to reflect the correct project. In my case I had to change the namespace from XAMLControlSample to MyCustomControls.
After moving the Templated Control to its correct location, let’s make sure that it can be consumed by each of our head projects:
- Update NuGet packages, importantly the Uno packages
- For each head project add a reference to the MyCustomControls project.
- Build and run each head project to make sure no compile errors (Note for WASM use the “Start without Debugging” option to launch the browser)
Once we’ve confirmed that each platform works without our Templated Control, it’s time to add an instance to the MainPage. Update the MainPage code to the following:
<Page x:Class="XAMLControlSample.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ctrls="using:MyCustomControls">
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<StackPanel>
<TextBlock Margin="20"
HorizontalAlignment="Center"
Text="XAML Templated Control"
FontSize="30" />
<ctrls:MultiSwitchControl
Width="400"
Height="400"
Background="Blue" />
</StackPanel>
</Grid>
</Page>
Run each of the head projects and verify that the MultiSwitchControl appears as a blue square.
Breaking Down the Templated Control
In the previous section we walked through creating a very simple Templated Control and demonstrated that through the power of Uno the same control can be used across iOS, Android, Windows and Web. Let’s take a look at how the Templated Control works, before we move on to building out our multi-switch control.
DefaultStyleKey for Implicit Style Lookup
The MultiSwitchControl.cs code file contains very little code. In fact, the only code it contains by default is a parameterless constructor that sets the DefaultStyleKey property.
public MultiSwitchControl()
{
this.DefaultStyleKey = typeof(MultiSwitchControl);
}
What’s not apparent here is that setting the DefaultStyleKey is critical to the loading of the control. When an instance of the MultiSwitchControl is created, unless the Style attribute has been set, the framework looks for a corresponding implicit style. An implicit style is one that doesn’t have an explicit Key defined. Instead, the Key for an implicit style is essentially the TargetType of the Style. For example in the Generic.xaml you’ll see that there is a Style defined with TargetType set to MultiSwitchControl.
<Style TargetType="local:MultiSwitchControl" >
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:MultiSwitchControl">
<Border
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
What’s important to note is that when the framework looks for the implicit Style, it doesn’t just assume that it should look for a Style with the TargetType matching that of the control. Instead it looks at the Type returned by the DefaultStyleKey property. Whilst this property is often just set to the Type of the control, there are cases where this isn’t the case.
Making your Implicit Style Explicit
One thing that annoys me about the item template that is used to generate the Templated Control is that it only defines an implicit Style for the control. The weakness of this is that it means that any developer wanting to override the Style has to copy the entire Style into their application. A better alternative is to make your Style explicit by giving it a Key, thus making it possible for other developers to inherit from your Style using the BasedOn attribute.
Of course, if you make your Style explicit, your Templated Control will no longer be able to find the Style without you explicitly referencing it. This is simple to overcome by defining an implicit style that inherits from your explicit Style.
If this all sounds a little complex, check out the amended Styles for the MultiSwitchControl below (there’s no code changes required to the MultiSwitchControl itself since it still relies on the implicit Style).
<Style x:Key="MultiSwitchControlDefaultStyle"
TargetType="local:MultiSwitchControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:MultiSwitchControl">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="local:MultiSwitchControl"
BasedOn="{StaticResource MultiSwitchControlDefaultStyle}" />
Designing the Template Control
At this point we have a lot of the infrastructure in place so we can get on with actually building our Templated Control. In this case we’re building a four-way switch control. It actually has five states: Off (Center), Up, Right, Down, Left, and as mentioned earlier we’re going to put minimal effort into the default design/layout of the control. We’ll show at the end of the process how easy it is for a developer consuming the control to override the Style and provide their own design without having to re-code the operation of the control (i.e. a true lookless control).
Simple Box Layout for the Template Control
To keep things simple the layout for the multi-switch that we’ll add to the MultiSwitchControlDefaultStyle will be a cross based on a 5×5 grid. There will be a box defined in the middle of the top row (Up), the center of the fifth column (Right), the middle of the bottom row (Down), the center of the first column (Left) and at the intersection of the third row and third column (Off). We’ve used a 5×5 layout to give a bit of spacing between the boxes, as you can see from the following image.
The updates Style defines each box using a Grid. At this stage a Border element would have sufficed. However, as you’ll see in the next step we’ll be nesting a couple of elements in the box to provide the visual context for when the user moves the mouse over the box, presses or clicks on the box, and when the box is selected.
<Style x:Key="MultiSwitchControlDefaultStyle"
TargetType="local:MultiSwitchControl">
<Setter Property="BorderBrush" Value="SteelBlue" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:MultiSwitchControl">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid x:Name="PART_Off"
Grid.Row="2"
Grid.Column="2"
Background="Transparent"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" />
<Grid x:Name="PART_Up"
Grid.Row="0"
Grid.Column="2"
Background="Transparent"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" />
<Grid x:Name="PART_Right"
Grid.Row="2"
Grid.Column="4"
Background="Transparent"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" />
<Grid x:Name="PART_Down"
Grid.Row="4"
Grid.Column="2"
Background="Transparent"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" />
<Grid x:Name="PART_Left"
Grid.Row="2"
Grid.Column="0"
Background="Transparent"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Note that in most cases where there is repeated XAML (for example setting the properties of Background, BorderBrush and BorderThickness) it pays to extract these into a Style that can simply be applied to all elements. However, in practice this both adds to the overhead of loading the control and you immediately run into limitations on the TemplateBinding markup extension. Attempting to extract these elements to a Style will result in a runtime exception that doesn’t seem to have a clear work around.
The next thing to note about the Style is that we’ve added a Setter for both BorderBrush and BorderThickness. The Setters define the default values for these properties, meaning that if the developer doesn’t explicitly set them on their instance of the MultiSwitchControl they’ll still have a value. If we didn’t do this, the default appearance of the MultiSwitchControl wouldn’t show the boxes since there would be no brush, and thickness would be 0.
The last thing to note about the Style is that each of the Grid elements have a Name attribute. In each case the value has the prefix “PART_” followed by the corresponding switch state eg PART_Off. This prefix was a convention adopted by WPF but subsequently dropped for Silverlight (see this post for some commentary on this topic), Windows Phone, UWP etc. Whilst you don’t have to adopt this prefix (you’ll see why in a minute) I still find it quite a clean way to identify parts of the Style that have to be there in order for the control to function correctly.
Visual States for the Templated Control
As mentioned earlier we want our Templated Control to be able to provide contextual feedback to the user. There are three things that we want to be able to do:
- Indicate when the user moves the mouse (UWP & WASM) over a box
- Indicate when the user clicks, presses, touches into a box
- Indicate when the user has selected a box
The first two of these we’ll pair together as they can represent the current state of the input device (aka pointer). This will be our CommonStates VisualStateGroup, to be consistent with other Windows controls, and will contain the following Visual States:
- Normal – pointer isn’t over any element or pressed down on any element
- PointerOverXXX – pointer has entered the area of element XXX
- PressedXXX – pointer has been pressed down on element XXX
Element XXX will be one of the Grid elements named in our Style, so our states will be PointerOverOff and PressedOff for the PART_Off Grid.
To track which box is currently selected we’ll create a second VisualStateGroup called SelectionStates, which will include Visual States with the naming convention SelectionXXX. So for the PART_Off Grid there will be a corresponding VisualState called SelectionOff. Additionally there will be one extra VisualState, SelectionNone, which represents the default state where no box has focus.
You might be asking at this point – why the need for two VisualStateGroups? or why not three? The answer to this is that VisualStateGroups should define mutually exclusive VisualStates; and that VisualStates from one group should not set the same properties as VisualStates from a different group. If we look at the scenarios above it’s very clear that we’d want to be able to specify which box is currently selected whilst being able to highlight a different box that the user may have moused over. What’s not immediately clear is why we’ve combined the PointerOver and the Pressed states into the one group. The reality is that we could have separated these into a third group. However, in this case we’re going to keep the implementation simple by assuming that the state of the pointer will either be PointerOver or Pressed and not both at the same time.
I mentioned earlier that each of the Grids we created for the different switch states were going to contain multiple elements. In fact we’re going to add three Border elements to each, with the resulting Grids all being similar to the following Part_Off Grid, where the element names have the switch state as their prefix eg OffPointerOver, OffPressed, OffSelection.
<Grid x:Name="PART_Off"
Grid.Row="2"
Grid.Column="2"
Background="Transparent"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Border x:Name="OffPointerOver"
Background="{TemplateBinding Background}"
Visibility="Collapsed" />
<Border x:Name="OffPressed"
Opacity="{TemplateBinding PressedOpacity}"
Background="{TemplateBinding Foreground}"
Visibility="Collapsed" />
<Border x:Name="OffSelection"
Background="{TemplateBinding Foreground}"
Visibility="Collapsed" />
</Grid>
Each Border has its Visibility property set to Collapsed. The OffPointerOver Border will be set to Visible when a Pointer enters the region of PART_Off. The OffPressed will be set to Visible when a Pointer is pressed inside the PART_Off. Lastly, the OffSelection will be set to Visible when the PART_Off is selected (i.e. the state of the switch is set to Off). All this of course has to be done with the corresponding visual states, as follows:
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOverOff">
<VisualState.Setters>
<Setter Target="OffPointerOver.Visibility" Value="Visible" />
</VisualState.Setters>
</VisualState>
...
<VisualState x:Name="PressedOff">
<VisualState.Setters>
<Setter Target="OffPressed.Visibility" Value="Visible" />
</VisualState.Setters>
</VisualState>
...
</VisualStateGroup>
<VisualStateGroup x:Name="SelectionStates">
<VisualState x:Name="SelectionNone" />
<VisualState x:Name="SelectionOff">
<VisualState.Setters>
<Setter Target="OffSelection.Visibility" Value="Visible" />
</VisualState.Setters>
</VisualState>
...
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
The visual states for the other parts are all similar, just with different names in the Target of the Setter.
Building the Functionality of the Templated Control
So far we’ve focused on getting the basic layout of the Templated Control sorted. This has included specifying the different visual states that map to both user interaction (i.e. pointer over and pressed) as well as the switch states (i.e. selection). What’s missing is that actual functionality of the MultiSwitchControl which will trigger the changes in the visual states and track what the current switch state is.
Current Switch State
To track the current state of the switch I’m going to define an enum called SwitchState, which will include the values Off, Up, Right, Down and Left. For completion I’ve added a None state to represent an invalid or non-set state. I’ll then add a Value dependency property which will track the current state of the switch. when the Value does change, the ValuePropertyChanged method will be invoked, which subsequently calls the UpdateSwitchState that is responsible for calling GoToState on the VisualStateManager. The name of the new VisualState is specified by concatenating the prefix “Selection” with the current switch Value. For example if the current Value is SwitchState.Off, the visual state name would be SelectionOff.
public enum SwitchState
{
None,
Off,
Up,
Right,
Down,
Left
}
private const string SelectionStatePrefix = "Selection";
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(nameof(Value), typeof(SwitchState),
typeof(MultiSwitchControl),
new PropertyMetadata(SwitchState.None, ValuePropertyChanged));
public SwitchState Value
{
get => (SwitchState)GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
private static void ValuePropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args)
{
var switchControl = dependencyObject as MultiSwitchControl;
switchControl?.UpdateSwitchState();
}
private void UpdateSwitchState()
{
VisualStateManager.GoToState(this, SelectionStatePrefix + this.Value, true);
}
Pointer Events in the Templated Control
A lot of the visual state changes are conditional on intercepting pointer activity entering, exiting, pressing and release on the Templated Control. To attach the correct event handlers we need to override the OnApplyTemplate method – this method is called to apply the template to the control, afterwhich the various parts of the template are available to interact with.
private IDictionary<UIElement, (SwitchState state, bool isInside, bool isPressed)> Parts { get; } = new Dictionary<UIElement, (SwitchState state, bool isInside, bool isPressed)>();
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
var switchStates = new[] { SwitchState.Off, SwitchState.Up, SwitchState.Right, SwitchState.Down, SwitchState.Left };
foreach (var s in switchStates)
{
SetupPart(s);
}
Value = SwitchState.Off;
}
private void SetupPart(SwitchState state)
{
var partName = PartPrefix + state;
var partOff = GetTemplateChild(partName) as UIElement;
if (partOff == null) throw new NullReferenceException($"{partName} expected in control template");
Parts[partOff] = (state: state, isInside: false, isPressed: false);
partOff.PointerPressed += PartPointerPressed;
partOff.PointerReleased += PartPointerReleased;
partOff.PointerEntered += PartPointerEntered;
partOff.PointerExited += PartPointerExited;
}
As the code above illustrates, the OnApplyTemplate method iterates through a list of switch states, invoking the SetupPart method, afterwhich it sets the default value of the switch to Off. The SetupPart method calls GetTemplateChild to retrieve the element generated by the corresponding template part. For example for the SwitchState.Off, the partName is “PART_Off”. Calling GetTemplateChild doesn’t retrieve the Grid from the ControlTemplate, it retrieves the Grid that was created as part of applying the ControlTemplate to the instance of the MultiSwitchControl.
The Parts dictionary is used to track the current state of each part of the MultiSwitchControl. More specifically it tracks whether a pointer is inside the part and whether the pointer has been pressed. As you’ll see in the next code snippet, these values are used to determine when different visual state changes are applied.
At this point we also wire up the event handlers for each of the pointer events. The expected flow is that a pointer will enter the part, it may then be pressed (which will capture the pointer), the pointer may then exit and/or release at some point in the future. If the pointer is released whilst still within the part, this will select the part and change the state of the MultiSwitchControl.
private void PartPointerEntered(object sender, PointerRoutedEventArgs e)
{
var partElement = sender as UIElement;
if (partElement == null)
{
return;
}
var part = Parts[partElement];
Parts[partElement] = (part.state, true, part.isPressed);
if (!part.isPressed)
{
VisualStateManager.GoToState(this, PointerOverStatePrefix + part.state, true);
}
}
private void PartPointerExited(object sender, PointerRoutedEventArgs e)
{
var partElement = sender as UIElement;
if (partElement == null)
{
return;
}
var part = Parts[partElement];
Parts[partElement] = (part.state, false, part.isPressed);
if (!part.isPressed)
{
VisualStateManager.GoToState(this, NormalState, true);
}
}
private void PartPointerPressed(object sender, PointerRoutedEventArgs e)
{
var partElement = sender as UIElement;
if (partElement == null)
{
return;
}
var part = Parts[partElement];
if (!part.isInside && !part.isPressed)
{
// Hack to deal with Android not firing events correctly
//VisualStateManager.GoToState(this, "Selection" + part.state, true);
Value = part.state;
VisualStateManager.GoToState(this, NormalState, true);
return;
}
Parts[partElement] = (part.state, part.isInside, true);
VisualStateManager.GoToState(this, PressedStatePrefix + part.state, true);
partElement.CapturePointer(e.Pointer);
}
private void PartPointerReleased(object sender, PointerRoutedEventArgs e)
{
var partElement = sender as UIElement;
if (partElement == null)
{
return;
}
partElement.ReleasePointerCaptures();
var part = Parts[partElement];
Parts[partElement] = (part.state, part.isInside, false);
if (part.isInside)
{
Value = part.state;
}
VisualStateManager.GoToState(this, NormalState, true);
}
What’s a TemplatePart?
Earlier in this post I mentioned that WPF had a pseudo standard for the naming of parts of the template that needed to exist. The more precise name for these elements are template parts and the reason that the naming convention is no longer widely adopted is that there is a more prescriptive way to communicate to developers the required parts of a control.
The TemplatePartAttribute should be used to define the name and, if necessary, the type of the content template that need to exist in order for the control to operate correctly. In the case of the MultiSwitchControl there are five template parts, so we add five instances of the TemplatePartAttribute to the MultiSwitchControl class.
[TemplatePart(Name = "PART_Off")]
[TemplatePart(Name = "PART_Up")]
[TemplatePart(Name = "PART_Right")]
[TemplatePart(Name = "PART_Down")]
[TemplatePart(Name = "PART_Left")]
public partial class MultiSwitchControl : Control
I’d love to stay that these attributes showed up in the visual designer in Visual Studio or Blend but the reality is that both designers are in a pretty messed up state right now, so I would count on getting any useful prompts. The best advice I’d give is that if you’re going to start messing with the template of a control, inspect the class for yourself and see what template parts are required.
Are We There Yet?
Yes, the good news is that we’ve got to a point where we have a functioning control. We’ve used all the power of UWP to separate the visuals (i.e. the ControlTemplate coupled with Visual States) from the underlying control functionality. The only real connection is via the named parts of the template.
The following GIFs illustrate the control running on Windows, Android and WASM:
Overriding the Style of a Templated Control
The last thing I wanted to illustrate is how it’s possible to adjust the layout and visual appearance of the switch control without impacting the way it works. In the App.xaml file in the shared project (i.e. not in the class library) I’ve copied across the Style for the MultiSwitchControl. I’ve subsequently modified the ControlTemplate as follows:
- Instead of multiple rows, all the boxes are now placed in 1 row
- Each box now has a rounded corner, effectively causing them to be circular in shape (this was admittedly a lazy way and I should really have made them ellipses).
<ControlTemplate TargetType="myCustomControls:MultiSwitchControl">
<Grid Background="Transparent"
DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}}">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid x:Name="PART_Left"
Grid.Column="0"
Background="Transparent"
CornerRadius="30"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Border x:Name="PART_Left_PointerOver"
Background="{TemplateBinding Background}"
Visibility="Collapsed" />
<Border x:Name="PART_Left_Pressed"
Opacity="{TemplateBinding PressedOpacity}"
Background="{TemplateBinding Foreground}"
Visibility="Collapsed" />
<Border x:Name="PART_Left_Selection"
Background="{TemplateBinding Foreground}"
Visibility="Collapsed" />
</Grid>
<Grid x:Name="PART_Down"
Grid.Column="2"
Background="Transparent"
CornerRadius="30"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Border x:Name="PART_Down_PointerOver"
Background="{TemplateBinding Background}"
Visibility="Collapsed" />
<Border x:Name="PART_Down_Pressed"
Opacity="{TemplateBinding PressedOpacity}"
Background="{TemplateBinding Foreground}"
Visibility="Collapsed" />
<Border x:Name="PART_Down_Selection"
Background="{TemplateBinding Foreground}"
Visibility="Collapsed" />
</Grid>
<Grid x:Name="PART_Off"
Grid.Column="4"
Background="Transparent"
CornerRadius="30"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Border x:Name="PART_Off_PointerOver"
Background="{TemplateBinding Background}"
Visibility="Collapsed" />
<Border x:Name="PART_Off_Pressed"
Opacity="{TemplateBinding PressedOpacity}"
Background="{TemplateBinding Foreground}"
Visibility="Collapsed" />
<Border x:Name="PART_Off_Selection"
Background="{TemplateBinding Foreground}"
Visibility="Collapsed" />
</Grid>
<Grid x:Name="PART_Up"
Grid.Column="6"
Background="Transparent"
CornerRadius="30"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Border x:Name="PART_Up_PointerOver"
Background="{TemplateBinding Background}"
Visibility="Collapsed" />
<Border x:Name="PART_Up_Pressed"
Opacity="{TemplateBinding PressedOpacity}"
Background="{TemplateBinding Foreground}"
Visibility="Collapsed" />
<Border x:Name="PART_Up_Selection"
Background="{TemplateBinding Foreground}"
Visibility="Collapsed" />
</Grid>
<Grid x:Name="PART_Right"
Grid.Column="8"
Background="Transparent"
CornerRadius="30"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Border x:Name="PART_Right_PointerOver"
Background="{TemplateBinding Background}"
Visibility="Collapsed" />
<Border x:Name="PART_Right_Pressed"
Opacity="{TemplateBinding PressedOpacity}"
Background="{TemplateBinding Foreground}"
Visibility="Collapsed" />
<Border x:Name="PART_Right_Selection"
Background="{TemplateBinding Foreground}"
Visibility="Collapsed" />
</Grid>
<VisualStateManager.VisualStateGroups>
...
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
The only other change I needed to make was in MainPage I needed to change the instance of the MultiSwitchControl to reference the Style that I’d added. Now when I run my sample application I can see that the MultiSwitchControl looks dramatically different, and yet still functions the same way.
Wrapping up the Templated Control
As you’ve hopefully seen in this post there’s huge potential with a Templated Control to build a component that can be heavily reused and more importantly restyled. The point of Templated Controls, or lookless controls, is that the restyling shouldn’t change the core functionality.
What excites me about the Uno platform is that this stuff just works. The entire Templated Control I’ve walked through works on Android, iOS, Windows and WASM – what other technology allows you to do that, with the same ability to retemplate a control.
Don’t forget the full source code is available on GitHub