Introduction
I had a requirement for a functionality where there would display read-only information until it was selected, when it would then display controls that would allow editing, and this Control
was in an ItemsControl
. Only one of these would be open at a time for editing. There was originally a lot of code in the ViewModel
to enable this functionality. I really did not like this and thought that this was the perfect application for a custom RadioButton
.
The Design
There are two parts for this control: the C# class that is derived from RadioButton
and the XAML that provides layout information. The C# class is as follows:
public class ContentChangeRadioButton : RadioButton
{
public object ToggleOnContent
{
get => GetValue(ToggleOnContentProperty);
set => SetValue(ToggleOnContentProperty, value);
}
public static readonly DependencyProperty ToggleOnContentProperty =
DependencyProperty.Register("ToggleOnContent", typeof(object),
typeof(ContentChangeRadioButton), new PropertyMetadata(null));
}
As can be seen all this class has to do is add the ToggleOnContent
DependencyProperty
to the RadioButton
.
Then the XAML for the control is as follows:
<Style TargetType="{x:Type local:ContentChangeRadioButton}">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ContentChangeRadioButton}">
<Grid>
<ContentPresenter x:Name="Part_ToggleOffContent"
Content="{TemplateBinding Content}" />
<ContentPresenter x:Name="Part_ToggleOnContent"
Content="{TemplateBinding ToggleOnContent}" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Part_ToggleOffContent"
Property="Visibility" Value="Collapsed" />
<Setter TargetName="Part_ToggleOnContent"
Property="Visibility" Value="Visible" />
</Trigger>
<Trigger Property="IsChecked" Value="False">
<Setter TargetName="Part_ToggleOnContent"
Property="Visibility" Value="Collapsed" />
<Setter TargetName="Part_ToggleOffContent"
Property="Visibility" Value="Visible" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
It just has two ContentPresenter
controls, and the Triggers
to control the Visibility
of these two controls based on the IsChecked
DependencyProperty
of the RadioButton
.
What I Learned
Originally I had much more complex C# code that would change the Visibility
of the the toggle on and toggle off content, and had to also handle the initial Visibility
of these Contents, but I did some thought and decided to try to use Triggers in the XAML instead, and this simplified the code extensively:
[TemplatePart(Name = ToggleOnContentName, Type = typeof(ContentControl))]
[TemplatePart(Name = ToggleOffContentName, Type = typeof(ContentControl))]
public class ContentChangeRadioButton : RadioButton
{
private const string ToggleOnContentName = "Part_ToggleOnContent";
private ContentPresenter _ToggleOnContent;
private const string ToggleOffContentName = "Part_ToggleOffContent";
private ContentPresenter _ToggleOffContent;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_ToggleOnContent = (ContentPresenter)GetTemplateChild(ToggleOnContentName);
_ToggleOffContent = (ContentPresenter)GetTemplateChild(ToggleOffContentName);
_ToggleOnContent.Visibility = (IsChecked == true)
? Visibility.Visible : Visibility.Collapsed;
_ToggleOffContent.Visibility = (IsChecked == true)
? Visibility.Collapsed : Visibility.Visible;
Checked += ContentChangeRadioButton_Checked;
Unchecked += ContentChangeRadioButton_Unchecked;
}
private void ContentChangeRadioButton_Unchecked(object sender, RoutedEventArgs e)
{
if (IsChecked == false)
{
_ToggleOnContent.Visibility = Visibility.Collapsed;
_ToggleOffContent.Visibility = Visibility.Visible;
}
}
private void ContentChangeRadioButton_Checked(object sender, RoutedEventArgs e)
{
_ToggleOnContent.Visibility = Visibility.Visible;
_ToggleOffContent.Visibility = Visibility.Collapsed;
}
static ContentChangeRadioButton()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ContentChangeRadioButton),
new FrameworkPropertyMetadata(typeof(ContentChangeRadioButton)));
}
public object ToggleOnContent
{
get { return (object)GetValue(ToggleOnContentProperty); }
set { SetValue(ToggleOnContentProperty, value); }
}
public static readonly DependencyProperty ToggleOnContentProperty =
DependencyProperty.Register("ToggleOnContent", typeof(object),
typeof(ContentChangeRadioButton),
new PropertyMetadata(null));
public bool ToggleState
{
get { return (bool)GetValue(ToggleStateProperty); }
set { SetValue(ToggleStateProperty, value); }
}
public static readonly DependencyProperty ToggleStateProperty =
DependencyProperty.Register("ToggleState", typeof(bool),
typeof(ContentChangeRadioButton),
new PropertyMetadata(false));
private static void ToggleStateChanged
(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (ContentChangeRadioButton)d;
if ((bool)e.NewValue)
{
control._ToggleOnContent.Visibility = Visibility.Visible;
control._ToggleOffContent.Visibility = Visibility.Collapsed;
}
else
{
control._ToggleOnContent.Visibility = Visibility.Collapsed;
control._ToggleOffContent.Visibility = Visibility.Visible;
}
}
}
There is one important DependencyProperty
, the ToggleOnContent
. The direct content of the RadioButton
is used for when the IsChecked
property is false
, and then the content specified when the IsChecked
property is true
is specified in this DependencyProperty
.
The OnApplyTemplate
method uses the GetTemplateChild
method to get references to the ContentPresenter
for both the unchecked and checked states, and also hooks up to the Checked
and Unchecked
events, and ensures that the Visibility
of the controls for the checked and unchecked states are correct for the current IsChecked
value.
The Checked
and Unchecked
event
handlers set the Visibility
of the two ContentPresenter
controls to the value appropriate for the IsChecked
value. You will notice that there is an additional check when the Unchecked
event
is fired to ensure that the control is truely in the IsChecked == false
. I had some issue with this when I had some content that included a ComboBox
, and this event would be fired when the ComboBox
dropdown closed.
Using the Control
To use this control, it simply has to be declared, and content specified for the ToggleOn
state (which is declared with the ToggleOnContent
attribute, and ToggleOff
which is declared in the direct content:
<local:ContentChangeRadioButton Margin="2"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Border Width="75"
Height="25"
HorizontalAlignment="Center"
VerticalAlignment="Center"
BorderBrush="Black"
BorderThickness="1">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="Toggle Off" />
</Border>
<local:ContentChangeRadioButton.ToggleOnContent>
<Border Width="100"
Height="30"
HorizontalAlignment="Center"
VerticalAlignment="Center"
BorderBrush="Blue"
BorderThickness="1">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="Toggle On" />
</Border>
</local:ContentChangeRadioButton.ToggleOnContent>
</local:ContentChangeRadioButton>
The single instance with ToggleOn
state is automatically accomplished because this control is derived from the RadioButton
.
Moving onto the ToggleButton Version
I later had a need to do something like this but more of a CheckBox type requirement since wanted independence. The code is almost identical:
public class ContentChangeToggleButton : ToggleButton
{
static ContentChangeToggleButton()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ContentChangeToggleButton),
new FrameworkPropertyMetadata(typeof(ContentChangeToggleButton)));
}
public object ToggleOnContent
{
get => (object) GetValue(ToggleOnContentProperty);
set => SetValue(ToggleOnContentProperty, value);
}
public static readonly DependencyProperty ToggleOnContentProperty =
DependencyProperty.Register("ToggleOnContent", typeof(object),
typeof(ContentChangeToggleButton),
new PropertyMetadata(null));
}
The code is identical except that it derives from ToggleButton
instead of RadioButton
. The XAML is basically also identical
The Sample
The sample has three of the RadioButton
controls, each one having the same content, one for on and one for off. The Content
is just different text inside a different Border
. There is also a ToggleButton control with the same Content except that for the title "Toggle" instead of "Radio".
You will notice that when one of these RadioButton
syles is clicked, it changes and if there is a control that had previously been selected, it will change to the Toggle Off state. Looking at the code, you will see that there is no code that is causing this change since the base Control
is of Type
RadioButton
. Of course the ToggleButton
style is independent of the RadioButton
controls
Conclusion
This was a very simple control to create, and I think it is a nice little control that makes a lot of sense. It is basically a simplified Expander
, but an Expander where the Header
disappears. The simplicity is what is nice about it. To a certain extent, I am not happy with how complex some of the Microsoft controls are since you need to do so much work to change them, and simpler controls can be more easily modified to look exactly like you want them to, and creating more sophisticated designs could easily be either done directly or styles can be created. This is a very simple control.
History
- 09/02/2017: Initial version
- 09/26/2017: Complete rework of design and adding of the
ToggleButton