This article takes you on the road I took to develop the ToggleSwitch control, from mock concept to a completed custom WPF control both in C# and VB. Some of the key flexibility concepts, like using VisualStates & SharedSizeGroup support, will be covered so that you can learn and reuse in your own controls.
Table of Contents
Background
I am always trying to keep a modern look and feel for the apps that I work on. Microsoft's focus is on UWP, and whilst WPF (and WinForms for that matter) are still current platforms, the common controls for these UI frameworks are not getting the full attention that they deserve to keep the look fresh and modern.
Toggle switches are found in most modern desktop, mobile, and gaming Operating Systems, including Windows 10, iOS, and Android amongst others. I needed a flexible and reusable Toggle Switch for my WPF apps. I could not find exactly what I wanted so I built one. The default styling mimics the Toggle Switch found in Windows 10, however the design on the control gives complete control over the look without touching the control's code.
Prerequisites
The projects for this article were built with the following in mind:
- C#6 minimum (Set in Properties > Build > Advanced > General > Language Version > C#6)
- Built using VS2017 (VS2015 will also load, build, and run)
- When you load the code the first time, you will need to restore Nuget Packages
The Concept
When reskinning, repurposing, or building new WPF controls, I like to create mocks/prototypes before diving into developing the custom control. Here is the one that I used:
In the above screenshot, we can see two different layouts:
- Header content on top of the switch, switch left set, switch label to the right of the switch
- Custom header on the left, switch on the right, switch label to the left of the switch
The header and the switch with state label could be two completely separate controls, however combining them into one allows for a larger mouse surface area for hover, clicking, etc.
I won't go into the XAML (no C#/VB code used) for the Mock project as this is not important. You can download the solution and look at the XAML used for the screenshot above and run it to see exactly how it mimics Windows 10.
The Design
The control could be a repurposed CheckBox
or ToggleButton
in a UserControl
or we could use the Control
base class and build all the functionality from scratch. Both the CheckBox
and the ToggleButton
contained most of the core plumbing required, so why re-invent the wheel?
The best choice was to use the ToggleButton
as the base of the ToggleSwitch
control over the CheckBox
. If we peek at the CheckBox
control's definition, we can see that it is derived from the ToggleButton
:
The control could just be the switch mechanism and the toggle state label, however, like the CheckBox
control, having header content as part of the control would keep the usage quick, simple, and clean.
Design Goals
Looking at how toggle switches are used on various devices and apps, like in the Windows 10 Notification Settings screenshot above, my goals were the following:
- The switch mechanism to mimic Windows 10 look, stretchable, can be reskinned
- The switch label can be placed left or right of the Switch mechanism
- Optional Header could contain content, not just text
- Header placement is selectable: left, right, above, below the Switch mechanism
- The Header content can be placed left or right of the Switch mechanism
- The Header content has horizontal adjustment: left, center, right, and stretch
- Adjustable internal spacing between Header, Switch, and Switch Label
- Properties to set brushes for Header, Switch, and Switch Label
- Label hot key support, i.e.: ALT+[letter]
A Quick Lookless Control Primer
This will be a quick primer for those who have worked with WPF, however have not developed lookless custom controls before. If you require a more in-depth introduction, then check out the Microsoft documentation on WPF Control Authoring[^].
Dependency Properties
Quote:
Represents a property that can be set through methods such as, styling, data binding, animation, and inheritance. - Microsoft Docs[^]
So, Dependency Properties are more than simple C# properties, they also include plumbing for the styling, databinding, animation systems and more that is transparent to the developer.
public static readonly DependencyProperty CheckedTextProperty =
DependencyProperty.Register(nameof(CheckedText),
typeof(string),
typeof(ToggleSwitch),
new PropertyMetadata("On",
new PropertyChangedCallback(OnCheckTextChanged)));
public string CheckedText
{
get { return (string)GetValue(CheckedTextProperty); }
set { SetValue(CheckedTextProperty, value); }
}
Public Shared ReadOnly CheckedTextProperty As DependencyProperty = _
DependencyProperty.Register(NameOf(CheckedText), _
GetType(String), _
GetType(ToggleSwitch), _
New PropertyMetadata("On", _
New PropertyChangedCallback(AddressOf OnCheckTextChanged)))
Public Property CheckedText() As String
Get
Return DirectCast(GetValue(CheckedTextProperty), String)
End Get
Set
SetValue(CheckedTextProperty, Value)
End Set
End Property
Using Dependency properties can appear to be a little messy and more involved than standard properties, but with a little help from a built-in Visual Studio (VS) snippet called propdb
for C#, or CTRL-K, CTRL-X > WPF > "Add a Dependency Property Registration" for VB, the code framework is inserted for you. Here is an example of the auto generated code by the VS snippet:
public int MyProperty
{
get { return (int)GetValue(MyPropertyProperty); }
set { SetValue(MyPropertyProperty, value); }
}
public static readonly DependencyProperty MyPropertyProperty =
DependencyProperty.Register("MyProperty",
typeof(int),
typeof(ownerclass),
new PropertyMetadata(0));
Public Property Prop1 As String
Get
Return GetValue(Prop1Property)
End Get
Set(ByVal value As String)
SetValue(Prop1Property, value)
End Set
End Property
Public Shared ReadOnly Prop1Property As DependencyProperty =
DependencyProperty.Register("Prop1",
GetType(String), GetType(),
New PropertyMetadata(Nothing))
Generic (default) Template
When developing custom controls, it is always advantageous to include a Generic Theme template. A Generic Theme template should hold the default look for the lookless control. The control automatically has an appearance associated with it. This means that you, or other developers, using the control does not have to manually reference the Control's Template. You can read more about Control Authoring Basics[^] in the Microsoft documentation.
The Generic.Xaml template is a Resource Dictionary file that must be placed in the \Themes folder directly off the project's root folder. If placed anywhere else, or of a different project file type, the Control Template will not be found.
For a Control to use the default Generic Template, you need to let the control know that it has one. I usually do it in the Control's constructor:
static ToggleSwitch()
{
DefaultStyleKeyProperty
.OverrideMetadata(typeof(ToggleSwitch),
new FrameworkPropertyMetadata(typeof(ToggleSwitch)));
}
Shared Sub New()
DefaultStyleKeyProperty _
.OverrideMetadata(GetType(ToggleSwitch), _
New FrameworkPropertyMetadata(GetType(ToggleSwitch)))
End Sub
Visual States
Quote:
Represents the visual appearance of the control when it is in a specific state. - Microsoft Docs[^]
Rather than setting Style Triggers[^] directly on individual controls, we can set up Visual States[^] in the Control Template that contain can StoryBoards[^] with Animations[^] for changing a group properties on multiple Properties & Controls.
<VisualState x:Name="MouseOver">
<Storyboard>
<DoubleAnimation To="0" Duration="0:0:0.2"
Storyboard.TargetName="normalBorder"
Storyboard.TargetProperty="(UIElement.Opacity)"/>
<DoubleAnimation To="1" Duration="0:0:0.2"
Storyboard.TargetName="hoverBorder"
Storyboard.TargetProperty="(UIElement.Opacity)"/>
<ObjectAnimationUsingKeyFrames Duration="0:0:0.2"
Storyboard.TargetName="optionMark"
Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{StaticResource ToggleSwitch.MouseOver.Glyph}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Duration="0:0:0.2"
Storyboard.TargetName="optionMarkOn"
Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{StaticResource ToggleSwitch.MouseOver.On.Glyph}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
NOTE: VisualStates do not support Template or Data Binding, only static values & StaticResources
. However, there is a workaround for this that will be discussed later in this article.
Laying Out Control Parts: Placement, Alignment, & Shared Grouping
This next section looks into how the ToggleSwitch
control implements selectable layout of elements of the control using properties.
Positioning
The ToggleSwitch
control is made up of two key parts:
- Header content (optional)
- Switch + state label
I will be referring to the "Header content" as "Content
" and the "Switch + state label" as ToggleButton
.
For the positioning of the header in relation to the switch, one part needs to be fixed or anchored. In this case, I will be anchoring the ToggleButton
and place the Content
around it. Below is a screenshot showing how this will work:
(ShowGridlines = true
to see layout of parts)
Translating this screenshot to code, there are three parts:
- Placement property
- Grid layout
- Visual States to alter the Grid layout properties
Generic Template
First, we need to associate the default Generic Template with the control:
private static readonly Type ctrlType = typeof(TestPositioning);
static TestPositioning()
{
DefaultStyleKeyProperty.OverrideMetadata
(ctrlType, new FrameworkPropertyMetadata(ctrlType));
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
}
Private Shared ReadOnly ctrlType As Type = GetType(TestPositioning)
Shared Sub New()
DefaultStyleKeyProperty.OverrideMetadata
(ctrlType, New FrameworkPropertyMetadata(ctrlType))
End Sub
Public Overrides Sub OnApplyTemplate()
MyBase.OnApplyTemplate()
End Sub
Control Class
Below is the code for managing the placement Dependency Property (DP). We track the PropertyChanged
event and change the VisualState
based on the new DP value.
private const Dock DefaultContentPlacementValue = Dock.Left;
private static readonly Type ctrlType = typeof(TestPositioning);
public static readonly DependencyProperty ContentPlacementProperty =
DependencyProperty.Register(nameof(ContentPlacement),
typeof(Dock), ctrlType,
new PropertyMetadata(DefaultContentPlacementValue,
OnContentPlacementPropertyChanged));
[Bindable(true)]
public Dock ContentPlacement
{
get { return (Dock)GetValue(ContentPlacementProperty); }
set { SetValue(ContentPlacementProperty, value); }
}
Private Const DefaultContentPlacementValue As Dock = Dock.Left
Private Shared ReadOnly ctrlType As Type = GetType(TestPositioning)
Public Shared ReadOnly ContentPlacementProperty As DependencyProperty _
= DependencyProperty.Register(NameOf(ContentPlacement), _
GetType(Dock), ctrlType, _
New PropertyMetadata(DefaultContentPlacementValue, _
AddressOf OnContentPlacementPropertyChanged))
<Bindable(True)>
Public Property ContentPlacement() As Dock
Get
Return GetValue(ContentPlacementProperty)
End Get
Set
SetValue(ContentPlacementProperty, Value)
End Set
End Property
When the PropertyChanged
event occurs, we need to notify the VisualState
Placement change for the Content
:
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
UpdatePlacementVisualState(ContentPlacement);
}
private static void OnContentPlacementPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var ctrl = d as TestPositioning;
if (ctrl != null)
ctrl.OnContentPlacementChanged((Dock)e.NewValue, (Dock)e.NewValue);
}
protected virtual void OnContentPlacementChanged(Dock newValue, Dock oldValue)
{
UpdatePlacementVisualState(newValue);
}
private void UpdatePlacementVisualState(Dock newPlacement)
{
GoToState(PlacementVisualState + newPlacement.ToString(), false);
}
internal bool GoToState(string stateName, bool useTransitions)
{
return VisualStateManager.GoToState(this, stateName, useTransitions);
}
Public Overrides Sub OnApplyTemplate()
MyBase.OnApplyTemplate()
UpdatePlacementVisualState(ContentPlacement)
End Sub
Private Shared Sub OnContentPlacementPropertyChanged(d As DependencyObject, _
e As DependencyPropertyChangedEventArgs)
Dim ctrl = TryCast(d, TestPositioning)
If ctrl IsNot Nothing Then
ctrl.OnContentPlacementChanged(e.NewValue, e.NewValue)
End If
End Sub
Protected Overridable Sub OnContentPlacementChanged(newValue As Dock, oldValue As Dock)
UpdatePlacementVisualState(newValue)
End Sub
Private Sub UpdatePlacementVisualState(newPlacement As Dock)
GoToState(PlacementVisualState + newPlacement.ToString(), False)
End Sub
Friend Function GoToState(stateName As String, useTransitions As Boolean) As Boolean
Return VisualStateManager.GoToState(Me, stateName, useTransitions)
End Function
NOTE: When there are multiple values, it is a good idea to have static or readonly variables to improve maintainability.
XAML Grid (Control Template)
To place the content around the fixed State ToggleButton
placement, we need to have a 3 x 3 grid with the fixed State ToggleButton
in the middle (Grid.Row="1"
, Grid.Column="1"
). This gives us four positions to place the Content
:
- Left (
Grid.Row="1"
, Grid.Column="0"
) - Right (
Grid.Row="1"
, Grid.Column="2"
) - Top (
Grid.Row="0"
, Grid.Column="1"
) - Bottom (
Grid.Row="2"
, Grid.Column="1"
)
Below is the XAML with the Content set with the default position set to Bottom:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition />
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition />
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ToggleButton Grid.Column="1" Grid.Row="1"
Margin="4 0" Content="Fixed"
Foreground="White" Background="Red"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"/>
<Border x:Name="ContentHost" Grid.Column="1" Grid.Row="2"
Background="Green"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}">
<TextBlock Text="Content" Foreground="White"/>
</Border>
</Grid>
VisualStates - Placement & Sizing
Now that we have the DP and the Grid Layout defined, the last thing to do is to have a VisualState
for each position targeting the Content's Grid Position - Column
, Row
, and Margin
(spacing):
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="ContentPlacement">
<VisualState x:Name="ContentPlacementAtLeft">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentHost"
Storyboard.TargetProperty="(Grid.Column)">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<sys:Int32>0</sys:Int32>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentHost"
Storyboard.TargetProperty="(Grid.Row)">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<sys:Int32>1</sys:Int32>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Duration="0"
Storyboard.TargetName="ContentHost"
Storyboard.TargetProperty="Margin">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Thickness>0 0 3 0</Thickness>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="ContentPlacementAtTop">
</VisualState>
<VisualState x:Name="ContentPlacementAtRight">
</VisualState>
<VisualState x:Name="ContentPlacementAtBottom">
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
Usage
Now that the control + template are completed, we use them. Below is the XAML for sample app that displays four controls, each with the ContentPlacement
property set to a different position. There is a GridSplitter
in the middle that you can drag left or right to see how the control reacts when resized.
<Window
x:Class="Positioning.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
mc:Ignorable="d"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:cc="clr-namespace:Positioning"
Title="Positioning Content | C#"
Height="300" Width="300" WindowStartupLocation="CenterScreen">
<Grid ShowGridLines="True">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.Resources>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Margin" Value="4"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<Style TargetType="{x:Type cc:TestPositioning}">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Margin" Value="10 0"/>
</Style>
</Grid.Resources>
<TextBlock Text="Left"/>
<cc:TestPositioning />
<TextBlock Text="Right" Grid.Row="1"/>
<cc:TestPositioning ContentPlacement="Right" Grid.Row="1"/>
<TextBlock Text="Top" Grid.Column="1"/>
<cc:TestPositioning ContentPlacement="Top" Grid.Column="1"/>
<TextBlock Text="Bottom" Grid.Row="1" Grid.Column="1"/>
<cc:TestPositioning ContentPlacement="Bottom" Grid.Row="1" Grid.Column="1"/>
<GridSplitter ResizeDirection="Columns" ShowsPreview="True"
HorizontalAlignment="Right" Width="3"
Background="Silver" Grid.RowSpan="2"/>
</Grid>
</Window>
Content Alignment
(ShowGridlines = true
to see layout of parts)
Next, we need to control the horizontal alignment of each of the two parts of the control. The code & XAML to achieve this is simply adding a Dependency Property and binding to it:
Control Class
We need to be able to track the Content Horizontal Alignment:
public static readonly DependencyProperty ContentHorizontalAlignmentProperty =
DependencyProperty.Register(nameof(ContentHorizontalAlignment),
typeof(HorizontalAlignment),
ctrlType,
new PropertyMetadata(DefaultContentHorizontalValue,
OnContentHorizontalAlignmentChanged));
[Bindable(true)]
public HorizontalAlignment ContentHorizontalAlignment
{
get { return (HorizontalAlignment)GetValue(ContentHorizontalAlignmentProperty); }
set { SetValue(ContentHorizontalAlignmentProperty, value); }
}
Public Shared ReadOnly ContentHorizontalAlignmentProperty As DependencyProperty = _
DependencyProperty.Register(NameOf(ContentHorizontalAlignment),
GetType(HorizontalAlignment),
ctrlType,
New PropertyMetadata(DefaultContentHorizontalValue,
AddressOf OnContentHorizontalAlignmentChanged))
<Bindable(True)>
Public Property ContentHorizontalAlignment() As HorizontalAlignment
Get
Return GetValue(ContentHorizontalAlignmentProperty)
End Get
Set
SetValue(ContentHorizontalAlignmentProperty, Value)
End Set
End Property
Now we can set the Placement
&/or Alignment
of the Content
. For the Alignment
to work, we also need to internally adjust the widths of the columns holding the two parts of the control. To do this, we need to add an internal Dependency Property to track when the Content
is on the Left or Right, then we need to set Column.Width = "*"
(fill):
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public static readonly DependencyProperty IsColumnStretchProperty =
DependencyProperty.Register(nameof(IsColumnStretch), typeof(bool), ctrlType, null);
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public bool IsColumnStretch
{
get { return (bool)GetValue(IsColumnStretchProperty); }
set { SetValue(IsColumnStretchProperty, value); }
}
<Browsable(False)>
<EditorBrowsable(EditorBrowsableState.Never)>
Public Shared ReadOnly IsColumnStretchProperty As DependencyProperty =
DependencyProperty.Register(NameOf(IsColumnStretch), GetType(Boolean), ctrlType, Nothing)
<Browsable(False)>
<EditorBrowsable(EditorBrowsableState.Never)>
Public Property IsColumnStretch() As Boolean
Get
Return GetValue(IsColumnStretchProperty)
End Get
Set
SetValue(IsColumnStretchProperty, Value)
End Set
End Property
When the PropertyChanged
event occurs for both ContentPlacement
and ContentHorizontalAlignment
, we need to notify the VisualState
Placement
change for the Content
:
private const string StretchVisualState = "ContentStretchAt";
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
CoerceColumnSizeChange();
UpdatePlacementVisualState(ContentPlacement);
}
private static void OnContentPlacementPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var ctrl = d as TestPositionSizing;
if (ctrl != null)
ctrl.OnContentPlacementChanged((Dock)e.NewValue, (Dock)e.NewValue);
}
protected virtual void OnContentPlacementChanged(Dock newValue, Dock oldValue)
{
CoerceColumnSizeChange();
UpdatePlacementVisualState(newValue);
}
private static void OnContentHorizontalAlignmentChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var ctrl = d as TestPositionSizing;
if (ctrl != null)
ctrl.CoerceColumnSizeChange();
}
private void CoerceColumnSizeChange()
{
SetValue(IsColumnStretchProperty, ContentPlacement == Dock.Left ||
ContentPlacement == Dock.Right);
}
private void UpdatePlacementVisualState(Dock newPlacement)
{
if (IsColumnStretch)
{
switch (newPlacement)
{
case Dock.Right:
case Dock.Left:
GoToState($"{StretchVisualState}{newPlacement.ToString()}", false);
break;
case Dock.Top:
case Dock.Bottom:
GoToState($"{StretchVisualState}Middle", false);
break;
}
}
else
{
GoToState($"{StretchVisualState}Middle", false);
}
GoToState(PlacementVisualState + newPlacement.ToString(), false);
}
internal bool GoToState(string stateName, bool useTransitions)
{
return VisualStateManager.GoToState(this, stateName, useTransitions);
}
Private Const StretchVisualState As String = "ContentStretchAt"
Public Overrides Sub OnApplyTemplate()
MyBase.OnApplyTemplate()
CoerceColumnSizing()
UpdatePlacementVisualState(ContentPlacement)
End Sub
Private Shared Sub OnContentPlacementPropertyChanged(d As DependencyObject, _
e As DependencyPropertyChangedEventArgs)
Dim ctrl = TryCast(d, TestPositionSizing)
If ctrl IsNot Nothing Then
ctrl.OnContentPlacementChanged(e.NewValue, e.NewValue)
End If
End Sub
Protected Overridable Sub OnContentPlacementChanged(newValue As Dock, oldValue As Dock)
CoerceColumnSizing()
UpdatePlacementVisualState(newValue)
End Sub
Private Shared Sub OnContentHorizontalAlignmentChanged(d As DependencyObject, _
e As DependencyPropertyChangedEventArgs)
Dim ctrl = TryCast(d, TestPositionSizing)
If ctrl IsNot Nothing Then
ctrl.CoerceColumnSizing()
End If
End Sub
Private Sub CoerceColumnSizing()
SetValue(IsColumnStretchProperty, ContentPlacement = Dock.Left OrElse _
ContentPlacement = Dock.Right)
End Sub
Private Sub UpdatePlacementVisualState(newPlacement As Dock)
If IsColumnStretch Then
Select Case newPlacement
Case Dock.Right, Dock.Left
GoToState(String.Format("{0}{1}", StretchVisualState,
newPlacement.ToString()), False)
Exit Select
Case Dock.Top, Dock.Bottom
GoToState(String.Format("{0}Middle", StretchVisualState), False)
Exit Select
End Select
Else
GoToState(String.Format("{0}Middle", StretchVisualState), False)
End If
GoToState(PlacementVisualState & newPlacement.ToString(), False)
End Sub
Friend Function GoToState(stateName As String, useTransitions As Boolean) As Boolean
Return VisualStateManager.GoToState(Me, stateName, useTransitions)
End Function
XAML Grid (Control Template)
The only change required to the previous part is that we need to set the Horizontal Alignment of the Content
to use the new control property ContentHorizontalAlignment
:
<Grid ShowGridLines="True">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="col0" Width="Auto"/>
<ColumnDefinition x:Name="col1" />
<ColumnDefinition x:Name="col2" Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition />
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ToggleButton Grid.Column="1" Grid.Row="1"
Margin="4 0" Content="Fixed"
Foreground="White" Background="Red"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"/>
<Border x:Name="ContentHost" Grid.Column="1" Grid.Row="2"
Background="Green"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalAlignment="{TemplateBinding ContentHorizontalAlignment}">
<TextBlock Text="Content" Foreground="White"/>
</Border>
</Grid>
Usage
Now that the control + template are completed, we use them. Below is the XAML for sample app that expands on the previous sample and shows the Left
, Center
, Right
, & Stretch
horizontal alignments of the Content
for each of the four placements:
<Grid ShowGridLines="True">
<Grid.Resources>
<Style x:Key="CustomControlStyle" TargetType="{x:Type cc:TestPositionSizing}">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="ContentHorizontalAlignment" Value="Left"/>
<Setter Property="Margin" Value="50 0"/>
</Style>
<Style TargetType="{x:Type cc:TestPositionSizing}"
BasedOn="{StaticResource CustomControlStyle}"/>
</Grid.Resources>
<Grid Style="{StaticResource GridStyle}">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.Resources>
<Style TargetType="{x:Type TextBlock}"
BasedOn="{StaticResource SubHeaderStyle}"/>
</Grid.Resources>
<cc:TestPositionSizing />
<TextBlock Text="Left"/>
<cc:TestPositionSizing ContentHorizontalAlignment="Center" Grid.Row="1"/>
<TextBlock Text="Center" Grid.Row="1"/>
<cc:TestPositionSizing ContentHorizontalAlignment="Right" Grid.Row="2"/>
<TextBlock Text="Right" Grid.Row="2"/>
</Grid>
<TextBlock Text="ContentPlacement: Right" Grid.Row="1"/>
<Grid Style="{StaticResource GridStyle}" Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.Resources>
<Style TargetType="{x:Type TextBlock}"
BasedOn="{StaticResource SubHeaderStyle}"/>
<Style TargetType="{x:Type cc:TestPositionSizing}"
BasedOn="{StaticResource CustomControlStyle}">
<Setter Property="ContentPlacement" Value="Right"/>
</Style>
</Grid.Resources>
<cc:TestPositionSizing ContentHorizontalAlignment="Left"/>
<TextBlock Text="Left"/>
<cc:TestPositionSizing ContentHorizontalAlignment="Center" Grid.Row="1"/>
<TextBlock Text="Center" Grid.Row="1"/>
<cc:TestPositionSizing ContentHorizontalAlignment="Right" Grid.Row="2"/>
<TextBlock Text="Right" Grid.Row="2"/>
</Grid>
</Grid>
SharedSizeGroup
If you are not familiar with SharedSizeGroup
, it is defined as:
Quote:
Gets or sets a value that identifies a ColumnDefinition or RowDefinition as a member of a defined group that shares sizing properties. - Microsoft Docs[^]
SharedSizeGroup
is handy when aligning labels and controls for data entry forms. Below is a mock example:
(ShowGridlines = true
to see layout of parts)
Property
First, we need a Dependency Property for setting the SharedGroupName
:
private static readonly string DefaultSharedSizeGroupName = string.Empty;
public static readonly DependencyProperty SharedSizeGroupNameProperty =
DependencyProperty.Register(nameof(SharedSizeGroupName),
typeof(string),
ctrlType,
null);
[Bindable(true)]
public string SharedSizeGroupName
{
get { return (string)GetValue(SharedSizeGroupNameProperty); }
set { SetValue(SharedSizeGroupNameProperty, value); }
}
Private Shared ReadOnly DefaultSharedSizeGroupName As String = String.Empty
Public Shared ReadOnly SharedSizeGroupNameProperty As DependencyProperty =
DependencyProperty.Register(NameOf(SharedSizeGroupName), _
GetType(String), _
ctrlType, _
Nothing)
<Bindable(True)>
Public Property SharedSizeGroupName() As String
Get
Return DirectCast(GetValue(SharedSizeGroupNameProperty), String)
End Get
Set
SetValue(SharedSizeGroupNameProperty, Value)
End Set
End Property
The Grid
is already set up in the previous parts, so next we need to set the VisualState
to enable the SharedGroup
layout.
This last one is a little bit trickier than the first as VisualStates do not support Template or Data Binding, only static values & StaticResources.
The work-around is to name the VisualState
Storyboard's animation with a name and set it in the Custom Control's code manually. We do this by searching the Control Template for the object with the matching name:
private const string SharedGroupStateName = "PART_SharedGroupSize";
private static void SharedGroupStateValue(TestPositionSizeSharedGroup ctrl,
Dock placement, bool IsBound = true)
{
var field = (DiscreteObjectKeyFrame)ctrl.Template?
.FindName(SharedGroupStateName + placement.ToString(), ctrl);
if (field != null)
{
var binding = new Binding(nameof(SharedSizeGroupName)) { Source = ctrl };
BindingOperations.SetBinding(field, ObjectKeyFrame.ValueProperty,
IsBound ? binding : new Binding());
}
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
SharedGroupStateValue(this, ContentPlacement);
CoerceContentSizing();
UpdatePlacementVisualState(ContentPlacement);
}
private static void OnContentPlacementPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var ctrl = d as TestPositionSizeSharedGroup;
if (ctrl != null)
{
var oldValue = (Dock)e.OldValue;
var newValue = (Dock)e.NewValue;
ChangeSharedGroupStateValue(ctrl, newValue, oldValue);
ctrl.OnContentPlacementChanged(newValue, oldValue);
}
}
private static void ChangeSharedGroupStateValue(TestPositionSizeSharedGroup ctrl,
Dock newValue, Dock oldValue)
{
SharedGroupStateValue(ctrl, oldValue, false);
SharedGroupStateValue(ctrl, newValue);
}
Private Const SharedGroupStateName As String = "PART_SharedGroupSize"
Private Shared Sub SharedGroupStateValue(ctrl As TestPositionSizeSharedGroup, _
placement As Dock, Optional IsBound As Boolean = True)
If ctrl.Template IsNot Nothing Then
Dim field = DirectCast(ctrl.Template _
.FindName(SharedGroupStateName & placement.ToString(), ctrl), _
DiscreteObjectKeyFrame)
If field IsNot Nothing Then
Dim binding = New Binding(NameOf(SharedSizeGroupName)) With {.Source = ctrl}
BindingOperations.SetBinding(field, ObjectKeyFrame.ValueProperty, _
If(IsBound, binding, New Binding()))
End If
End If
End Sub
Public Overrides Sub OnApplyTemplate()
MyBase.OnApplyTemplate()
SharedGroupStateValue(Me, ContentPlacement)
CoerceContentSizing()
UpdatePlacementVisualState(ContentPlacement)
End Sub
Private Shared Sub OnContentPlacementPropertyChanged(d As DependencyObject, _
e As DependencyPropertyChangedEventArgs)
Dim ctrl = TryCast(d, TestPositionSizeSharedGroup)
If ctrl IsNot Nothing Then
Dim oldValue = DirectCast(e.OldValue, Dock)
Dim newValue = DirectCast(e.NewValue, Dock)
ChangeSharedGroupStateValue(ctrl, newValue, oldValue)
ctrl.OnContentPlacementChanged(newValue, oldValue)
End If
End Sub
Private Shared Sub ChangeSharedGroupStateValue(ctrl As TestPositionSizeSharedGroup, _
newValue As Dock, oldValue As Dock)
SharedGroupStateValue(ctrl, oldValue, False)
SharedGroupStateValue(ctrl, newValue)
End Sub
VisualStates - SharedGroup
We need to set the correct Column's SharedSizeGroup
property via the VisualState
Storyboard's named animation. One for each placement position:
<VisualStateGroup x:Name="ContentPlacement">
<VisualState x:Name="ContentPlacementAtLeft">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="col0"
Storyboard.TargetProperty="SharedSizeGroup">
<DiscreteObjectKeyFrame x:Name="PART_SharedGroupSizeLeft"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="ContentPlacementAtTop">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="col1"
Storyboard.TargetProperty="SharedSizeGroup">
<DiscreteObjectKeyFrame x:Name="PART_SharedGroupSizeTop"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="ContentPlacementAtRight">
<Storyboard>
<ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="col1"
Storyboard.TargetProperty="SharedSizeGroup">
<DiscreteObjectKeyFrame x:Name="PART_SharedGroupSizeRight"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="ContentPlacementAtBottom">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="col1"
Storyboard.TargetProperty="SharedSizeGroup">
<DiscreteObjectKeyFrame x:Name="PART_SharedGroupSizeBottom"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
Usage
This sample is a little different from the first two. Below is the snippet showing how to use the SharedGroup
:
<Window.Resources>
<sys:String x:Key="SharedSizeCol1">SharedGroup1</sys:String>
</Window.Resources>
<Grid ShowGridLines="True" Grid.IsSharedSizeScope="True">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.Resources>
<Style x:Key="GridStyle" TargetType="{x:Type Grid}">
<Setter Property="ShowGridLines" Value="True"/>
<Setter Property="Margin" Value="10 30 10 0"/>
</Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Margin" Value="4"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<Style TargetType="Label">
<Setter Property="Margin" Value="0 5"/>
<Setter Property="Padding" Value="0 0 5 0"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="HorizontalAlignment" Value="Right"/>
</Style>
<Style TargetType="TextBox">
<Setter Property="Grid.Column" Value="1"/>
<Setter Property="Margin" Value="0 5"/>
<Setter Property="Padding" Value="4"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
</Style>
<Style x:Key="CustomControlStyle" TargetType="{x:Type cc:TestPositionSizeSharedGroup}">
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Grid.ColumnSpan" Value="2"/>
<Setter Property="Margin" Value="0 5"/>
</Style>
<Style TargetType="{x:Type cc:TestPositionSizeSharedGroup}"
BasedOn="{StaticResource CustomControlStyle}"/>
</Grid.Resources>
<TextBlock Text="SharedSizeGroup: Set" Grid.Column="1"/>
<Grid Style="{StaticResource GridStyle}" Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" SharedSizeGroup="{StaticResource SharedSizeCol1}"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.Resources>
<Style TargetType="{x:Type cc:TestPositionSizeSharedGroup}"
BasedOn="{StaticResource CustomControlStyle}">
<Setter Property="SharedSizeGroupName"
Value="{StaticResource SharedSizeCol1}"/>
</Style>
</Grid.Resources>
<Label Content="Field _1" Target="{Binding ElementName=Value21}"/>
<TextBox x:Name="Value21" Text="Value 1"/>
<cc:TestPositionSizeSharedGroup Grid.Row="1"/>
<cc:TestPositionSizeSharedGroup ContentHorizontalAlignment="Center" Grid.Row="2"/>
<cc:TestPositionSizeSharedGroup ContentHorizontalAlignment="Right" Grid.Row="3"/>
<Label Content="Field _5" Target="{Binding ElementName=Value25}" Grid.Row="4"/>
<TextBox x:Name="Value25" Text="Value 5" Grid.Row="4"/>
<Label Content="Field _6" Target="{Binding ElementName=Value26}" Grid.Row="5"/>
<TextBox x:Name="Value26" Text="Value 6" Grid.Row="5"/>
<GridSplitter ResizeDirection="Columns" ShowsPreview="True"
HorizontalAlignment="Right" Width="3"
Background="Silver" Grid.RowSpan="7"/>
</Grid>
<GridSplitter ResizeDirection="Columns" ShowsPreview="True"
HorizontalAlignment="Right" Width="3"
Background="Silver" Grid.RowSpan="2"/>
</Grid>
Property Explorer
The ToggleSwitch
control exposes properties to allow full control over both the header content and the switch and label. These can be viewed in the XAML Property Explorer.
To make the properties easier to find, we can allocate Dependency Properties to Categories using the CategoryAttribute[^] and a hint using the DescriptionAttribute[^]. The Description is displayed when hovering the mouse cursor over the property in the XAML Property Explorer.
[Bindable(true)]
[Description("Gets or sets the graphical switch checked background brush."),
Category(ctrlName)]
public Brush CheckedBackground
{
get { return (Brush)GetValue(CheckedBackgroundProperty); }
set { SetValue(CheckedBackgroundProperty, value); }
}
<Bindable(True)>
<Description("Gets or sets the graphical switch checked background brush."),
Category(ctrlName)>
Public Property CheckedBackground() As Brush
Get
Return DirectCast(GetValue(CheckedBackgroundProperty), Brush)
End Get
Set
SetValue(CheckedBackgroundProperty, Value)
End Set
End Property
And here, we can see a complete grouped list of the important properties available in the XAML Property Explorer:
Using the ToggleSwitch Control
I have included a demonstration project that has 6 x C#/VB examples of how to use the control focusing on specific key functionality:
- The first three are a repeat of the layout samples above with the
ToggleSwitch
: Positioning
, Alignment
and SharedSizeGroup
- Collections - The control bound to a collection of
SettingModel
in a ViewModel
- Styling/Skinning - A custom looking
ToggleSwitch
- Reproduction of Windows 10 Notification Settings
Download the solution and look at the code to see how the control is implemented in each example.
Positioning
This was covered in the section above Laying out Control Parts: Positioning.
Positioning & Alignment
This was covered in the section above Laying out Control Parts: Alignment.
Positioning, Alignment, & SharedSizeGroup
This was covered in the section above Laying out Control Parts: SharedSizeGroup.
Collections of Toggle Switches
This sample binds to a SettingModel
collection in a ViewModel
and the XAML uses a DataTemplate
layout the ToggleSwitch
.
SettingModel
public class SettingModel : ObservableObject
{
private string title;
public string Title
{
get { return title; }
set { Set(ref title, value); }
}
private string yesChoice;
public string YesChoice
{
get { return yesChoice; }
set { Set(ref yesChoice, value); }
}
private string noChoice;
public string NoChoice
{
get { return noChoice; }
set { Set(ref noChoice, value); }
}
private bool isChecked = true;
public bool IsChecked
{
get { return isChecked; }
set { Set(ref isChecked, value); }
}
}
Public Class SettingModel : Inherits ObservableObject
Private mTitle As String
Public Property Title() As String
Get
Return mTitle
End Get
Set
[Set](mTitle, Value)
End Set
End Property
Private mYesChoice As String
Public Property YesChoice() As String
Get
Return mYesChoice
End Get
Set
[Set](mYesChoice, Value)
End Set
End Property
Private mNoChoice As String
Public Property NoChoice() As String
Get
Return mNoChoice
End Get
Set
[Set](mNoChoice, Value)
End Set
End Property
Private mIsChecked As Boolean = True
Public Property IsChecked() As Boolean
Get
Return mIsChecked
End Get
Set
[Set](mIsChecked, Value)
End Set
End Property
End Class
ViewModel
public class ListPageViewModel
{
public ObservableCollection<SettingModel> Settings { get; } =
new ObservableCollection<SettingModel>
{
new SettingModel
{
Title = "Setting 1", IsChecked = false, NoChoice = "No", YesChoice = "Yes"
},
new SettingModel {
Title = "Setting 2", IsChecked = false, NoChoice = "Up", YesChoice = "Down"
}
};
}
Public Class ListPageViewModel
Public Sub New()
Settings = New ObservableCollection(Of SettingModel)() From {
New SettingModel() With {
.Title = "Setting 1", .IsChecked = False,
.NoChoice = "No", .YesChoice = "Yes"},
New SettingModel() With {
.Title = "Setting 2", .IsChecked = False,
.NoChoice = "Up", .YesChoice = "Down"}
}
End Sub
Public ReadOnly Property Settings() As ObservableCollection(Of SettingModel)
End Class
XAML
<Page.DataContext>
<vm:ListPageViewModel/>
</Page.DataContext>
<Page.Resources>
<sys:String x:Key="SharedSizeCol3">listCol</sys:String>
<DataTemplate x:Key="LeftSettingsTemplate">
<cc:ToggleSwitch Grid.ColumnSpan="2"
Content="{Binding Title}"
HeaderHorizontalAlignment="Stretch"
HeaderContentPlacement="Left"
IsChecked="{Binding IsChecked}"
SwitchContentPlacement="Right"
CheckedText="{Binding YesChoice}"
UncheckedText="{Binding NoChoice}" />
</DataTemplate>
<DataTemplate x:Key="RightSettingsTemplate">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"
SharedSizeGroup="{StaticResource SharedSizeCol3}"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<cc:ToggleSwitch Grid.ColumnSpan="2"
Content="{Binding Title}"
TextBlock.TextAlignment="Right"
HeaderHorizontalAlignment="Stretch"
HeaderContentPlacement="Right"
HeaderPadding="0 0 10 0"
IsChecked="{Binding IsChecked}"
SwitchContentPlacement="Left"
CheckedText="{Binding YesChoice}"
UncheckedText="{Binding NoChoice}"
SharedSizeGroupName="{StaticResource SharedSizeCol3}"/>
</Grid>
</DataTemplate>
</Page.Resources>
<Grid Margin="10 0 10 10">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ScrollViewer Margin="0 0 5 0" HorizontalScrollBarVisibility="Disabled">
<ItemsControl ItemsSource="{Binding Settings}"
ItemTemplate="{StaticResource LeftSettingsTemplate}"/>
</ScrollViewer>
<ScrollViewer Grid.Column="1" Grid.Row="1" Grid.RowSpan="8"
Cursor="Hand"
HorizontalScrollBarVisibility="Disabled">
<ItemsControl ItemsSource="{Binding Settings}"
ItemTemplate="{StaticResource RightSettingsTemplate}"/>
</ScrollViewer>
</Grid>
Custom Styling/Skinning
With this sample, I have tried to make the ToggleSwitch
control look different without modifying the default template + use template brushes to change the brushes based on the control's brushes for each of the two states.
<Grid.Resources>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Margin" Value="4"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<Style x:Key="TextBlockStyle" TargetType="{x:Type TextBlock}">
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="FontSize" Value="16"/>
<Setter Property="FontWeight" Value="Light"/>
<Setter Property="FontStyle" Value="Italic"/>
<Setter Property="Foreground"
Value="{Binding RelativeSource={RelativeSource
AncestorType=cc:ToggleSwitch},
Path=UncheckedForeground}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource
AncestorType=cc:ToggleSwitch},
Path=IsChecked}"
Value="True">
<Setter Property="Foreground"
Value="{Binding RelativeSource={RelativeSource
AncestorType=cc:ToggleSwitch},
Path=CheckedForeground}"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="PathStyle" TargetType="{x:Type Path}">
<Setter Property="Fill"
Value="{Binding RelativeSource={RelativeSource
AncestorType=cc:ToggleSwitch},
Path=UncheckedForeground}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource
AncestorType=cc:ToggleSwitch},
Path=IsChecked}"
Value="True">
<Setter Property="Fill"
Value="{Binding RelativeSource={RelativeSource
AncestorType=cc:ToggleSwitch},
Path=CheckedForeground}"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="GridStyle" TargetType="{x:Type Grid}">
<Setter Property="Background"
Value="{Binding RelativeSource={RelativeSource
AncestorType=cc:ToggleSwitch},
Path=UncheckedBackground}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource
AncestorType=cc:ToggleSwitch},
Path=IsChecked}"
Value="True">
<Setter Property="Background"
Value="{Binding RelativeSource={RelativeSource
AncestorType=cc:ToggleSwitch},
Path=CheckedBackground}"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style TargetType="{x:Type cc:ToggleSwitch}">
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="HeaderHorizontalAlignment" Value="Stretch"/>
<Setter Property="HeaderContentPlacement" Value="Top"/>
<Setter Property="HeaderPadding" Value="0 0 0 4"/>
<Setter Property="SwitchHorizontalAlignment" Value="Center"/>
<Setter Property="SwitchContentPlacement" Value="Right"/>
<Setter Property="SwitchPadding" Value="8 0 0 0"/>
<Setter Property="SwitchWidth" Value="100"/>
<Setter Property="CheckHorizontalAlignment" Value="Right"/>
<Setter Property="CheckedBackground" Value="Red"/>
<Setter Property="CheckedForeground" Value="Yellow"/>
<Setter Property="CheckedBorderBrush" Value="Yellow"/>
<Setter Property="UncheckedBackground" Value="Yellow"/>
<Setter Property="UncheckedForeground" Value="Red"/>
<Setter Property="UncheckedBorderBrush" Value="Red"/>
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property="Foreground" Value="MediumPurple"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="CheckedText" Value="Yes"/>
<Setter Property="UncheckedText" Value="No"/>
</Style>
</Grid.Resources>
<cc:ToggleSwitch x:Name="CheckedSwitch" IsChecked="True">
<cc:ToggleSwitch.Content>
<Grid Style="{StaticResource GridStyle}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Viewbox Width="32" Height="32" Margin="4">
<Grid>
<Path Style="{StaticResource PathStyle}"
Data ="[trimmed for briefity]"/>
</Grid>
</Viewbox>
<TextBlock Style="{StaticResource TextBlockStyle}" Grid.Column="1">
Custom<LineBreak/>Header
</TextBlock>
</Grid>
</cc:ToggleSwitch.Content>
</cc:ToggleSwitch>
Replicating Windows 10 Notification Settings Screen
This sample required a hover background for the ToggleSwitch
. As the ToggleSwitch
does not support Highlighting on mouse over, so I have implemented the functionality in the ListItem
DataTemplate
using Triggers
:
<SolidColorBrush x:Key="Hover.Enter.Brush" Color="#FFF2F2F2" />
<SolidColorBrush x:Key="Hover.Exit.Brush" Color="#01FFFFFF" />
<Storyboard x:Key="Hover.Enter.Storyboard">
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0:0:0"
Value="{StaticResource Hover.Enter.Brush}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="Hover.Exit.Storyboard">
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0:0:0"
Value="{StaticResource Hover.Exit.Brush}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<Style x:Key="HoverBorder" TargetType="Border">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Margin" Value="0 4"/>
<Setter Property="Padding" Value="10 2"/>
<Style.Triggers>
<EventTrigger RoutedEvent="Mouse.MouseEnter">
<BeginStoryboard Storyboard="{StaticResource Hover.Enter.Storyboard}" />
</EventTrigger>
<EventTrigger RoutedEvent="Mouse.MouseLeave">
<BeginStoryboard Storyboard="{StaticResource Hover.Exit.Storyboard}" />
</EventTrigger>
</Style.Triggers>
</Style>
<DataTemplate DataType="{x:Type m:AppSettingModel}">
<Border Style="{StaticResource HoverBorder}">
<cc:ToggleSwitch IsChecked="{Binding IsChecked}">
<cc:ToggleSwitch.Content>
</cc:ToggleSwitch.Content>
</cc:ToggleSwitch>
</Border>
</DataTemplate>
Summary
I have tried to keep the amount of code in the article to the bare minimum. There is more that is not discussed, however is straight forward. I recommend downloading the solution and looking at the source code - I have left a few gems to be found.
The ToggleSwitch
control is a complete control that you can include in your own projects. It is also an example showing just how easy it can be to repurpose existing WPF controls into new professional-looking controls with a few Dependency properties and a custom style.
Enjoy!
History
- v1.0 - 14th November, 2017 - Initial release