4
\$\begingroup\$

Note: See Verticle WPF icon button that used Segoe MDL2 font for icon for an example using Segoe MDL2 for the icons.

I need a button that changes appearance based on the state of a Boolean. A previous attempt is here: WPF button with xaml-defined icon that changes with state

In this implementation, I do this creating a custom control based off Button that does NOT use a ControlTemplate. From what I understand, using ControlTemplate completely replaces the default control template, so you have to create the entire style again.

Another method is to put a button inside the ControlTemplate and bind the properties to the DependencyProperties of the custom control and the Button it is based on, e.g. https://www.codeproject.com/Tips/773386/WPF-ImageButton. The problem with this is that properties automatically set such as IsEnabled for a command do not get automatically applied to the internal Button

So I use ContentTemplate and trigger changes via DataTrigger instead of ControlTemplate and ControlTemplate.Triggers

This is the class with the dependency properties:

public class VerticalImageButton : Button
{
    static VerticalImageButton()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(VerticalImageButton), new FrameworkPropertyMetadata(typeof(VerticalImageButton)));
    }

    public bool State
    {
        get { return (bool)GetValue(StateProperty); }
        set { SetValue(StateProperty, value); }
    }

    public static readonly DependencyProperty StateProperty =
        DependencyProperty.Register("State",
        typeof(bool), typeof(VerticalImageButton), new PropertyMetadata(null));

    public ControlTemplate IconOn
    {
        get { return (ControlTemplate)GetValue(IconOnProperty); }
        set { SetValue(IconOnProperty, value); }
    }
    public static readonly DependencyProperty IconOnProperty =
        DependencyProperty.Register("IconOn",
        typeof(ControlTemplate), typeof(VerticalImageButton), new PropertyMetadata(null));

    public ControlTemplate IconOff
    {
        get { return (ControlTemplate)GetValue(IconOffProperty); }
        set { SetValue(IconOffProperty, value); }
    }
    public static readonly DependencyProperty IconOffProperty =
        DependencyProperty.Register("IconOff",
        typeof(ControlTemplate), typeof(VerticalImageButton), new PropertyMetadata(null));

    public string ContentOn
    {
        get { return (string)GetValue(ContentOnProperty); }
        set { SetValue(ContentOnProperty, value); }
    }
    public static readonly DependencyProperty ContentOnProperty =
        DependencyProperty.Register("ContentOn",
        typeof(string), typeof(VerticalImageButton), new PropertyMetadata(null));

    public string ContentOff
    {
        get { return (string)GetValue(ContentOffProperty); }
        set { SetValue(ContentOffProperty, value); }
    }
    public static readonly DependencyProperty ContentOffProperty =
        DependencyProperty.Register("ContentOff",
        typeof(string), typeof(VerticalImageButton), new PropertyMetadata(null));
}

Note that the icons are XAML resources wrapped in ControlTemplate, see: WPF button with xaml-defined icon that changes with state

Now in the Generic.xaml file that is created when you make a custom control in VS:

<Style TargetType="{x:Type local:VerticalImageButton}" BasedOn="{StaticResource {x:Type Button}}">
    <Style.Resources>
        <Style x:Key="buttonText" TargetType="{x:Type TextBlock}">
            <Setter Property="Text" Value="{Binding ContentOff, RelativeSource={RelativeSource AncestorType=Button}}"/>
            <Style.Triggers>
                <DataTrigger Binding="{Binding State, RelativeSource={RelativeSource AncestorType=Button}}"  Value="True">
                    <Setter Property="Text" Value="{Binding ContentOn, RelativeSource={RelativeSource AncestorType=Button}}"/>
                </DataTrigger>
                <DataTrigger Binding="{Binding State, RelativeSource={RelativeSource AncestorType=Button}}" Value="False">
                    <Setter Property="Text" Value="{Binding ContentOff, RelativeSource={RelativeSource AncestorType=Button}}"/>
                </DataTrigger>
            </Style.Triggers>
        </Style>
        <Style x:Key="buttonIcon" TargetType="{x:Type ContentControl}">
            <Setter Property="Template" Value="{Binding IconOff, RelativeSource={RelativeSource AncestorType=Button}}"/>
            <Style.Triggers>
                <DataTrigger Binding="{Binding State, RelativeSource={RelativeSource AncestorType=Button}}"  Value="True">
                    <Setter Property="Template" Value="{Binding IconOn, RelativeSource={RelativeSource AncestorType=Button}}"/>
                </DataTrigger>
                <DataTrigger Binding="{Binding State, RelativeSource={RelativeSource AncestorType=Button}}" Value="False">
                    <Setter Property="Template" Value="{Binding IconOff, RelativeSource={RelativeSource AncestorType=Button}}"/>
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </Style.Resources>
    <Setter Property="ContentTemplate">
        <Setter.Value>
            <DataTemplate>
                <StackPanel Orientation="Vertical" Margin="2" >
                    <Viewbox Width="36">
                        <ContentControl Style="{DynamicResource buttonIcon}"/>
                    </Viewbox>
                    <TextBlock Style="{DynamicResource buttonText}" VerticalAlignment="Center"/>
                </StackPanel>
            </DataTemplate>
        </Setter.Value>
    </Setter>
    <Setter Property="Width" Value="61"/>
    <Setter Property="Margin" Value="2"/>
</Style>

One problem is that because it is based on {StaticResource {x:Type Button}}, it won't inherit the style of the container - e.g. when putting into a Toolbar. For this case, you can just add the toolbar style, Style="{StaticResource {x:Static ToolBar.ButtonStyleKey}}" when laying out the buttons.

Finally, the use of the control is quite easy:

<local:VerticalImageButton
    x:Name="TestButton"
    Style="{StaticResource {x:Static ToolBar.ButtonStyleKey}}"
    Command="{Binding TestCommand}"
    IconOff="{StaticResource TestOnIcon}"
    IconOn="{StaticResource TestOffIcon}"
    ContentOff="Connect"
    ContentOn="Disconnect"
    State="{Binding IsTestOn}"/>

Two icons if someone wants to implement. Note the binding on the fill. This makes the icon change colour with the button, and is needed when binding the button to an ICommand so that the icon also goes gray if the command cannot be executed.

<ControlTemplate x:Key="TestOffIcon">
    <Canvas
            Width="76" 
            Height="76" 
            Clip="F1 M 0,0L 76,0L 76,76L 0,76L 0,0">
        <Path 
                Width="36.7542"
                Height="36.7542" 
                Canvas.Left="19.6229" 
                Canvas.Top="19.6229" 
                Stretch="Fill"  
                Fill="{Binding RelativeSource={RelativeSource AncestorType=Control}, Path=Foreground}" 
                Data="F1 M 25.7639,28.0031L 20.0866,22.3258C 19.4683,21.7075 19.4683,20.705 20.0866,20.0866C 20.705,19.4683 21.7075,19.4683 22.3258,20.0867L 28.0031,25.7639C 32.3443,22.5092 38.5302,22.856 42.4783,26.8042L 26.8041,42.4784C 22.856,38.5302 22.5092,32.3443 25.7639,28.0031 Z M 49.1958,33.5217C 53.144,37.4699 53.4908,43.6557 50.2361,47.9969L 55.9133,53.6742C 56.5317,54.2925 56.5317,55.295 55.9133,55.9134C 55.295,56.5317 54.2925,56.5317 53.6742,55.9134L 47.9969,50.2361C 43.6557,53.4908 37.4698,53.1441 33.5216,49.1959L 36.8804,45.8371L 34.0814,43.0381C 33.1539,42.1107 33.1539,40.6069 34.0814,39.6794C 35.0089,38.7519 36.5127,38.7519 37.4402,39.6794L 40.2392,42.4784L 42.4783,40.2392L 39.6794,37.4402C 38.7519,36.5127 38.7519,35.009 39.6794,34.0815C 40.6069,33.154 42.1106,33.154 43.0381,34.0815L 45.8371,36.8804L 49.1958,33.5217 Z "/>
    </Canvas>
</ControlTemplate>
<ControlTemplate x:Key="TestOnIcon">
    <Canvas
            Width="76" 
            Height="76"
            Clip="F1 M 0,0L 76,0L 76,76L 0,76L 0,0">
        <Path 
                Width="35.9625"
                Height="35.9625"
                Canvas.Left="20.0187"
                Canvas.Top="20.0187" 
                Stretch="Fill" 
                Fill="{Binding RelativeSource={RelativeSource AncestorType=Control}, Path=Foreground}" 
                Data="F1 M 27.6073,29.8464L 20.4825,22.7216C 19.8641,22.1033 19.8641,21.1008 20.4825,20.4825C 21.1008,19.8641 22.1033,19.8641 22.7216,20.4825L 29.8464,27.6073C 34.1877,24.3526 40.3735,24.6993 44.3217,28.6475L 28.6475,44.3217C 24.6993,40.3735 24.3526,34.1877 27.6073,29.8464 Z M 47.7483,32.0742C 51.6965,36.0223 52.0433,42.2082 48.7885,46.5494L 55.5175,53.2784C 56.1358,53.8967 56.1358,54.8992 55.5175,55.5175C 54.8992,56.1359 53.8967,56.1359 53.2783,55.5175L 46.5494,48.7886C 42.2081,52.0433 36.0223,51.6965 32.0741,47.7484L 35.4329,44.3896L 32.6339,41.5906C 31.7064,40.6631 31.7064,39.1594 32.6339,38.2319C 33.5614,37.3044 35.0652,37.3044 35.9927,38.2319L 38.7916,41.0308L 41.0308,38.7917L 38.2319,35.9927C 37.3044,35.0652 37.3044,33.5614 38.2319,32.634C 39.1594,31.7065 40.6631,31.7065 41.5906,32.6339L 44.3896,35.4329L 47.7483,32.0742 Z "/>
    </Canvas>
</ControlTemplate>

Update

There were a few mistakes that have been fixed with my edit. Note that in the style one must use RelativeSource={RelativeSource AncestorType=Button} with the binding to make it refer to the button dependency properties.

Fixed on and off icon references.

\$\endgroup\$
1
  • \$\begingroup\$ What was your question? \$\endgroup\$
    – Ben Voigt
    Commented Feb 12, 2020 at 19:15

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.