Click here to Skip to main content
15,867,488 members
Articles / Desktop Programming / WPF

Flexible WPF ToggleSwitch Lookless Control in C# & VB

Rate me:
Please Sign up or sign in to vote.
5.00/5 (36 votes)
15 Nov 2017CPOL11 min read 81.8K   4.2K   41   24
A Modern Toggle Switch - From mock concept to a full custom WPF control that you can plug into your own apps
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.

Windows 10 Notification Settings

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:

Mock Concepts

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

ToggleSwitch Section Breakdown

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:

WPF CheckBox definition

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.

C#
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); }
}
VB
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:

C#
public int MyProperty
{
    get { return (int)GetValue(MyPropertyProperty); }
    set { SetValue(MyPropertyProperty, value); }
}

// Using a DependencyProperty as the backing store for MyProperty.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty MyPropertyProperty =
    DependencyProperty.Register("MyProperty",
                                typeof(int),
                                typeof(ownerclass),
                                new PropertyMetadata(0));
VB
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:

C#
static ToggleSwitch()
{
    DefaultStyleKeyProperty
        .OverrideMetadata(typeof(ToggleSwitch), 
                          new FrameworkPropertyMetadata(typeof(ToggleSwitch)));
}
VB
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.

XML
<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

ToggleSwitch Section Breakdown

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:

Content positioning

(ShowGridlines = true to see layout of parts)

Translating this screenshot to code, there are three parts:

  1. Placement property
  2. Grid layout
  3. Visual States to alter the Grid layout properties
Generic Template

First, we need to associate the default Generic Template with the control:

C#
private static readonly Type ctrlType = typeof(TestPositioning);

static TestPositioning()
{
	DefaultStyleKeyProperty.OverrideMetadata
    (ctrlType, new FrameworkPropertyMetadata(ctrlType));
}

public override void OnApplyTemplate()
{
	base.OnApplyTemplate();
	// set our initial VisualState here
}
VB
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()
	' set our initial VisualState here
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.

C#
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); }
}
VB
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:

C#
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);
}
VB
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:

XML
<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):

XML
<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">
		    <!-- top placement storyboard [..trimmed...] -->
        </VisualState>
        <VisualState x:Name="ContentPlacementAtRight">
		    <!-- right placement storyboard [..trimmed...] -->
        </VisualState>
        <VisualState x:Name="ContentPlacementAtBottom">
		    <!-- bottom placement storyboard [..trimmed...] -->
        </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.

XML
<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 /> <!-- DEFAULT -->

        <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

Positioning & 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:

C#
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); }
}
VB
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):

C#
[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); }
}
VB
<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:

C#
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);
}
VB
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:

XML
<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:

XML
<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 /> <!-- DEFAULT -->
		<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:

SharedSizeGrouping

(ShowGridlines = true to see layout of parts)

Property

First, we need a Dependency Property for setting the SharedGroupName:

C#
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); }
}
VB
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:

C#
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);
}
VB
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:

XML
<VisualStateGroup x:Name="ContentPlacement">
    <VisualState x:Name="ContentPlacementAtLeft">
        <Storyboard>
            <!-- trimmed to focus on named animation -->
            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="col0"
                                           Storyboard.TargetProperty="SharedSizeGroup">
                <DiscreteObjectKeyFrame x:Name="PART_SharedGroupSizeLeft"/>
            </ObjectAnimationUsingKeyFrames>
        </Storyboard>
    </VisualState>
    <VisualState x:Name="ContentPlacementAtTop">
        <Storyboard>
            <!-- trimmed to focus on named animation -->
            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="col1"
                                           Storyboard.TargetProperty="SharedSizeGroup">
                <DiscreteObjectKeyFrame x:Name="PART_SharedGroupSizeTop"/>
            </ObjectAnimationUsingKeyFrames>
        </Storyboard>
    </VisualState>
    <VisualState x:Name="ContentPlacementAtRight">
        <Storyboard>
            <!-- trimmed to focus on named animation -->
            <ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="col1"
                                           Storyboard.TargetProperty="SharedSizeGroup">
                <DiscreteObjectKeyFrame x:Name="PART_SharedGroupSizeRight"/>
            </ObjectAnimationUsingKeyFrames>
        </Storyboard>
    </VisualState>
    <VisualState x:Name="ContentPlacementAtBottom">
        <Storyboard>
            <!-- trimmed to focus on named animation -->
            <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:

XML
<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"/> <!-- DEFAULT -->

        <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.

C#
[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); }
}
VB
<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:

ToggleSwitch Properties

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:

  1. The first three are a repeat of the layout samples above with the ToggleSwitch: Positioning, Alignment and SharedSizeGroup
  2. Collections - The control bound to a collection of SettingModel in a ViewModel
  3. Styling/Skinning - A custom looking ToggleSwitch
  4. 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

Positioning

This was covered in the section above Laying out Control Parts: Positioning.

Positioning & Alignment

Positioning & Alignment

This was covered in the section above Laying out Control Parts: Alignment.

Positioning, Alignment, & SharedSizeGroup

Positioning, Alignment, & SharedSizeGroup

This was covered in the section above Laying out Control Parts: SharedSizeGroup.

Collections of Toggle Switches

PositioningCollections of Toggle Switches

This sample binds to a SettingModel collection in a ViewModel and the XAML uses a DataTemplate layout the ToggleSwitch.

SettingModel

C#
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); }
    }
}
VB
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

C#
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"
        }
    };
}
VB
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"}
            ' trimmed for briefity
        }
    End Sub

    Public ReadOnly Property Settings() As ObservableCollection(Of SettingModel)

End Class

XAML

XML
<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

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.

XML
<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

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:

XML
<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>
                <!-- [trimmed for briefity] -->
            </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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead
Australia Australia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Bit2 Developer8-Feb-21 5:50
professionalBit2 Developer8-Feb-21 5:50 
QuestionBroken Download Pin
Andreas Schramm6-Sep-20 19:55
Andreas Schramm6-Sep-20 19:55 
PraiseCreated Account to Say Thanks! Pin
Member 1486799719-Jun-20 9:39
Member 1486799719-Jun-20 9:39 
QuestionThree state Pin
Member 1482776420-May-20 12:27
Member 1482776420-May-20 12:27 
QuestionUsing control through code Pin
CMy3rs6-Mar-19 4:05
CMy3rs6-Mar-19 4:05 
QuestionHow to use toggleswitch in my project Pin
Member 1383605920-May-18 9:58
Member 1383605920-May-18 9:58 
AnswerRe: How to use toggleswitch in my project Pin
Graeme_Grant21-Jun-18 0:41
mvaGraeme_Grant21-Jun-18 0:41 
BugDid not compile Pin
Clifford Nelson22-Mar-18 6:22
Clifford Nelson22-Mar-18 6:22 
GeneralRe: Did not compile Pin
Graeme_Grant22-Mar-18 12:17
mvaGraeme_Grant22-Mar-18 12:17 
GeneralRe: Did not compile Pin
Clifford Nelson22-Mar-18 14:12
Clifford Nelson22-Mar-18 14:12 
GeneralRe: Did not compile Pin
Graeme_Grant22-Mar-18 14:17
mvaGraeme_Grant22-Mar-18 14:17 
GeneralRe: Did not compile Pin
Clifford Nelson22-Mar-18 15:46
Clifford Nelson22-Mar-18 15:46 
Well for me I am looking for a control, not something like MVVM Lite. Seldom do I start a project from scratch so a whole framework does not make sense. This project is supposedly about a control not a framework. I usually start by looking at the code, and running it. I will not read the details unless I have some question. With your project, I could not figure where to start. God this is supposed to be a control, how much code do you need to demonstrate a control, and if you are going to make it so big, you should put a lot more consideration in usability. I am not going to spend hours trying to figure out what you are doing because, among other things, I may not like your solution. Also, why do you need MVVM Light to demonstrate a control. You can write components. They may not be as bullet proof as the MVVM Light, but as long as the work well enough. I have done a lot of sample projects, and needed some additional features that I would normally get from a framework, but to make life easier for the person who may download my sample I roll my own. Even did that with the article I wrote on the Event Aggregator, after all PRISM is quite bloated. So if you are not going to go to the effort of creating good sample projects, I consider your article as waste. Even the projects provided by companies like Infragistics are not this difficult to figure out or run. You create an easy to understand project that will at least let me understand the capabilities of the control and how to easily use it in my application, I consider this a really bad article.

I go to a lot of effort to build sample project that are easy to understand. Go and look at my projects and show me one that is not pretty straight forward. I would expect the same from other authors.

The download NuGet packages was disabled (which is what I saw first time I looked), and had to go to manage. First time I have ever had to go to manage on an existing project. So it runs. But now have to find where it is. But from what I can see it does not do what I want (maybe if I could see the XAML). I just wanted the ToggleButton part, not the label or other content. I tend to prefer simpler to complex because quite often need to customize. I still do not know if your control can be resized well. I also do not like the text giving the state outside the combo box. So I am underwelmed by your design in solving my problem and still have to search the half a meg of code to find what I am looking for. I is bascially a very different control from what I designed.

modified 22-Mar-18 22:03pm.

GeneralRe: Did not compile Pin
Graeme_Grant22-Mar-18 16:06
mvaGraeme_Grant22-Mar-18 16:06 
GeneralRe: Did not compile Pin
Clifford Nelson22-Mar-18 16:12
Clifford Nelson22-Mar-18 16:12 
GeneralRe: Did not compile Pin
Graeme_Grant23-Mar-18 15:00
mvaGraeme_Grant23-Mar-18 15:00 
GeneralRe: Did not compile Pin
Clifford Nelson25-Mar-18 10:40
Clifford Nelson25-Mar-18 10:40 
GeneralRe: Did not compile Pin
Graeme_Grant26-Mar-18 12:16
mvaGraeme_Grant26-Mar-18 12:16 
GeneralRe: Did not compile Pin
Clifford Nelson26-Mar-18 12:23
Clifford Nelson26-Mar-18 12:23 
PraiseMy Vote of 5 Pin
Nicky Carpenter15-Nov-17 7:16
Nicky Carpenter15-Nov-17 7:16 
GeneralRe: My Vote of 5 Pin
Graeme_Grant15-Nov-17 10:16
mvaGraeme_Grant15-Nov-17 10:16 
GeneralRe: My Vote of 5 Pin
Graeme_Grant15-Nov-17 10:16
mvaGraeme_Grant15-Nov-17 10:16 
GeneralMy vote of 5 Pin
DrABELL14-Nov-17 4:56
DrABELL14-Nov-17 4:56 
GeneralRe: My vote of 5 Pin
Graeme_Grant14-Nov-17 11:19
mvaGraeme_Grant14-Nov-17 11:19 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.