Click here to Skip to main content
15,868,141 members
Articles / Desktop Programming / WPF

Expander Control with On/Off Content using RadioButton Control

Rate me:
Please Sign up or sign in to vote.
4.53/5 (8 votes)
2 Oct 2017CPOL3 min read 7.7K   107   7   1
This is a control that is like a Expander Control but there is both Expanded and not Expanded Content. It demonstrates some concepts on how to easily create controls whose functionality can be mostly implemented in the XAML with the code behind being mostly DependencyProperty definitions.

Introduction

I had a case where a design had content which would be visible under a header in both the on and off cases. This was a design that was modified from a design that only had content in the ToggleOn case. An Expander worked fine for this case, but not for the new design. Thus, a new Control was designed based on a ContentControl. However, in this case, the Control was a lot more complex than just this simple Control since it has been customized for a specific application there were DependencyProperty definitions that support the Content of the Expander Header.

As I proceeded with the design, I learned a lot about the possibilities, in particular how to simplify, and avoid using code-behind. The initial version had almost all code behind, but I was able to remove the code-behind for everything but the DependencyProperty definition by using Template Binding and Triggers.

The Design

The XAML is as follows:

XML
<Style TargetType="{x:Type local:ToggleButtonContentBanner}">
    <Setter Property="Background" Value="Transparent" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:ToggleButtonContentBanner}">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition />
                        <RowDefinition />
                        <RowDefinition />
                    </Grid.RowDefinitions>
                    <Border x:Name="PART_HeaderBorder"
                            Grid.ColumnSpan="3"
                            Height="{Binding HeaderHeight,
                             RelativeSource={RelativeSource TemplatedParent}}"
                            HorizontalAlignment="Stretch"
                            VerticalAlignment="Stretch"
                            Background="{Binding Background,
                             RelativeSource={RelativeSource TemplatedParent}}">
                        <ToggleButton x:Name="StateToggleButton"
                                  HorizontalAlignment="Stretch"
                                  VerticalAlignment="Stretch"
                                  HorizontalContentAlignment="Stretch"
                                  VerticalContentAlignment="Stretch"
                                  IsChecked="{Binding IsChecked,
                                       RelativeSource={RelativeSource TemplatedParent}}"
                                  Style="{StaticResource {x:Static ToolBar.ToggleButtonStyleKey}}">
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="Auto" />
                                    <ColumnDefinition Width="Auto" />
                                    <ColumnDefinition Width="Auto" />
                                    <ColumnDefinition />
                                </Grid.ColumnDefinitions>
                                <ContentControl x:Name="PART_HeaderImageControl"
                                                Grid.Column="0"
                                                Content="{TemplateBinding HeaderImage}" />
                                <TextBlock x:Name="PART_HeaderTextControl"
                                           Grid.Column="1"
                                           VerticalAlignment="Center"
                                           FontSize="{Binding FontSize,
                                              RelativeSource={RelativeSource TemplatedParent}}"
                                           Foreground="{Binding Foreground,
                                              RelativeSource={RelativeSource TemplatedParent}}"
                                           Text="{TemplateBinding HeaderText}" />
                                <Button x:Name="HelpButton"
                                        Grid.Column="2"
                                        Width="24"
                                        Height="24"
                                        Margin="5"
                                        Padding="0"
                                        HorizontalAlignment="Left"
                                        Command="{TemplateBinding HelpCommand}"
                                        Style="{StaticResource {x:Static ToolBar.ButtonStyleKey}}">
                                    <Border Width="22"
                                            Height="22"
                                            BorderBrush="{Binding Foreground,
                                               RelativeSource={RelativeSource TemplatedParent}}"
                                            BorderThickness="2"
                                            CornerRadius="12">
                                        <TextBlock HorizontalAlignment="Center"
                                               VerticalAlignment="Center"
                                               FontSize="16"
                                               FontWeight="Bold"
                                               Foreground="{Binding Foreground,
                                                  RelativeSource={RelativeSource TemplatedParent}}"
                                               Text="?" />
                                    </Border>
                                </Button>
                            </Grid>
                        </ToggleButton>
                    </Border>
                    <ContentControl x:Name="PART_ToggleOnContent"
                                    Grid.Row="1"
                                    Grid.ColumnSpan="3"
                                    Content="{TemplateBinding ToggleOnContent}" />
                    <ContentControl x:Name="PART_ToggleOffContent"
                                    Grid.Row="2"
                                    Grid.ColumnSpan="3"
                                    Content="{TemplateBinding ToggleOffContent}" />
                </Grid>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsChecked" Value="true">
                        <Setter TargetName="PART_ToggleOnContent"
                Property="Visibility" Value="Visible" />
                        <Setter TargetName="PART_ToggleOffContent"
                Property="Visibility" Value="Collapsed" />
                    </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>

Most of this code is for the header or the part, and the two ContentControl definitions for the XAML content for the toggle on and toggle off states. There is also the Trigger that will cause the Visibility of these two ContentControl definitions to be Visible or Collapsed depending on the value of the IsChecked property for the Root control.

The C# code for this Control is almost all just DependencyProperty definitions:

C#
public class ToggleButtonContentBanner : ContentControl
{
    static ToggleButtonContentBanner()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(ToggleButtonContentBanner),
            new FrameworkPropertyMetadata(typeof(ToggleButtonContentBanner)));
    }

    public object ToggleOnContent
    {
        get { return (object)GetValue(ToggleOnContentProperty); }
        set { SetValue(ToggleOnContentProperty, value); }
    }

    public static readonly DependencyProperty ToggleOnContentProperty =
        DependencyProperty.Register("ToggleOnContent", typeof(object),
            typeof(ToggleButtonContentBanner), new PropertyMetadata(null));

    public DependencyObject ToggleOffContent
    {
        get { return (DependencyObject)GetValue(ToggleOffContentProperty); }
        set { SetValue(ToggleOffContentProperty, value); }
    }

    public static readonly DependencyProperty ToggleOffContentProperty =
        DependencyProperty.Register("ToggleOffContent", typeof(DependencyObject),
            typeof(ToggleButtonContentBanner), new PropertyMetadata(null));

    public string HeaderText
    {
        get { return (string)GetValue(HeaderTextProperty); }
        set { SetValue(HeaderTextProperty, value); }
    }

    public static readonly DependencyProperty HeaderTextProperty =
        DependencyProperty.Register("HeaderText", typeof(string),
            typeof(ToggleButtonContentBanner),
            new PropertyMetadata(null));
    public Brush HeaderBackground
    {
        get { return (Brush)GetValue(HeaderBackgroundProperty); }
        set { SetValue(HeaderBackgroundProperty, value); }
    }

    public static readonly DependencyProperty HeaderBackgroundProperty =
        DependencyProperty.Register("HeaderBackground", typeof(Brush),
            typeof(ToggleButtonContentBanner), new PropertyMetadata(null));

    public double HeaderHeight
    {
        get { return (double)GetValue(HeaderHeightProperty); }
        set { SetValue(HeaderHeightProperty, value); }
    }

    public static readonly DependencyProperty HeaderHeightProperty =
        DependencyProperty.Register("HeaderHeight", typeof(double),
            typeof(ToggleButtonContentBanner), new PropertyMetadata(0.0d));

    public object HeaderImage
    {
        get { return GetValue(HeaderImageProperty); }
        set { SetValue(HeaderImageProperty, value); }
    }

    public static readonly DependencyProperty HeaderImageProperty =
        DependencyProperty.Register("HeaderImage", typeof(object),
            typeof(ToggleButtonContentBanner),
            new PropertyMetadata(null));
    public ICommand HelpCommand
    {
        get { return (ICommand)GetValue(HelpCommandProperty); }
        set { SetValue(HelpCommandProperty, value); }
    }

    public static readonly DependencyProperty HelpCommandProperty =
        DependencyProperty.Register("HelpCommand", typeof(ICommand),
            typeof(ToggleButtonContentBanner), new PropertyMetadata(null));

    public bool IsChecked
    {
        get { return (bool)GetValue(IsCheckedProperty); }
        set { SetValue(IsCheckedProperty, value); }
    }

    public static readonly DependencyProperty IsCheckedProperty =
        DependencyProperty.Register("IsChecked", typeof(bool),
            typeof(ToggleButtonContentBanner), new PropertyMetadata(false));
}

Normally, if a Style is created with a ControlTemplate, the customization only allows use of properties that already exist for the control. This can be limiting. It is also possible to create controls using a UserControl. This is a very flexible Control, but from what I understand, a UserControl has a greater performance impact--at least when I was working for Microsoft on a project, we were not allowed to use the UserControl because of this performance penalty. The significant advantage of the UserControl is that it is easier to design since Visual Studio WYSIWYG designer for the UserControl but not for a ControlTemplate or a DataTemplates.

Both a ControlTemplate and a UserControl have the advantage of being able to write code-behind, but that is sort of like putting code behind on Window--It is considered better to not have code behind. As can be seen, this control has more available than available in a ContentControl that this Control is derived from, but there is nothing in the code-behind besides the DependencyProperty definitions. This is done by defining each needed DependencyProperty. Then this DependencyProperty is available for use by using RelativeSource TemplatedParent Binding. It is also possible to the TemplateBinding but I have run into difficulty directly using TemplateBinding when values were updated. I have so far not run into problems using the RelativeSource Binding.

Using the Code

This Control is very special purpose and a lot of the attributes are to support this particular requirement, such as the HeaderText, HeaderImage, HelpCommand. The IsChecked, ToggleOnContent and ToggleOffContent are the properties that actually provide this custom type Expander capability:

XML
<local:ToggleButtonContentBanner VerticalAlignment="Top"
                                 HeaderText="Experiment Applications"
                                 HelpCommand="{Binding HelpCommand}"
                                 IsChecked="True"
                                 Style="{StaticResource CreateExperimentExpander}">
    <local:ToggleButtonContentBanner.HeaderImage>
        <Image Source="MyImage.jpg"/>
    </local:ToggleButtonContentBanner.HeaderImage>
    <local:ToggleButtonContentBanner.ToggleOffContent>
        <Border Height="100"
                Background="Red">
            <TextBlock HorizontalAlignment="Center"
                       VerticalAlignment="Center"
                       Text="The Toggle Banner is On" />
        </Border>
    </local:ToggleButtonContentBanner.ToggleOffContent>
    <local:ToggleButtonContentBanner.ToggleOnContent>
        <Border Height="100"
                Background="Green">
            <TextBlock HorizontalAlignment="Center"
                       VerticalAlignment="Center"
                       Text="The Toggle Banner is Off" />
        </Border>
    </local:ToggleButtonContentBanner.ToggleOnContent>
</local:ToggleButtonContentBanner>

Points of Interest

The following are some of the things that should be takeaways:

  1. When designing a custom Control, it may be possible to eliminate the code behind by using Template Binding and Triggers.
  2. Create new properties that can be used with Triggers and Template Binding, and then used when the Control is used.
  3. TemplateBinding may work, but may have update issues, so probably better to use RelativeSource TemplatedParent.

History

  • 10/02/2017: Initial version

License

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


Written By
Software Developer (Senior) Clifford Nelson Consulting
United States United States
Has been working as a C# developer on contract for the last several years, including 3 years at Microsoft. Previously worked with Visual Basic and Microsoft Access VBA, and have developed code for Word, Excel and Outlook. Started working with WPF in 2007 when part of the Microsoft WPF team. For the last eight years has been working primarily as a senior WPF/C# and Silverlight/C# developer. Currently working as WPF developer with BioNano Genomics in San Diego, CA redesigning their UI for their camera system. he can be reached at qck1@hotmail.com.

Comments and Discussions

 
GeneralOne one star vote....Something wrong.... Pin
Medtronic WPF Developer26-Mar-18 8:12
Medtronic WPF Developer26-Mar-18 8:12 

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.