2
\$\begingroup\$

This is a button that changes text and icon according to a Boolean state. for example, a play / pause button or a connect / disconnect button. The icon uses Segoe MDL2 Assets as the default font, but you can edit the template to change to another!

The code:

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

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

    public Orientation Orientation
    {
        get { return (Orientation)GetValue(OrientationProperty); }
        set { SetValue(OrientationProperty, value); }
    }

    public String TextOn
    {
        get { return (String)GetValue(TextOnProperty); }
        set { SetValue(TextOnProperty, value); }
    }

    public String IconOn
    {
        get { return (String)GetValue(IconOnProperty); }
        set { SetValue(IconOnProperty, value); }
    }

    public String TextOff
    {
        get { return (String)GetValue(TextOffProperty); }
        set { SetValue(TextOffProperty, value); }
    }

    public String IconOff
    {
        get { return (String)GetValue(IconOffProperty); }
        set { SetValue(IconOffProperty, value); }
    }

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

    public static readonly DependencyProperty OrientationProperty =
        DependencyProperty.Register("Orientation",
            typeof(Orientation),
            typeof(StateImageButton),
            new PropertyMetadata(Orientation.Vertical));

    public static readonly DependencyProperty TextOnProperty =
        DependencyProperty.Register("TextOn",
        typeof(string),
        typeof(StateImageButton),
        new PropertyMetadata(""));

    public static readonly DependencyProperty IconOnProperty =
        DependencyProperty.Register("IconOn",
        typeof(string),
        typeof(StateImageButton),
        new PropertyMetadata(""));

    public static readonly DependencyProperty TextOffProperty =
        DependencyProperty.Register("TextOff",
            typeof(string),
            typeof(StateImageButton),
            new PropertyMetadata(""));

    public static readonly DependencyProperty IconOffProperty =
        DependencyProperty.Register("IconOff",
            typeof(string),
            typeof(StateImageButton),
            new PropertyMetadata(""));
}

The style is below. It is based on the ToolBar button style, as I prefer this, but you can change it to a normal button style too. Note that MinWidth is bound to ActualHeight so the button is at least as wide as it is tall.

<Style TargetType="{x:Type local:StateImageButton}" BasedOn="{StaticResource {x:Static ToolBar.ButtonStyleKey}}">
    <Setter Property="MinWidth" Value="{Binding Path=ActualHeight, RelativeSource={RelativeSource Self}}"/>
    <Setter Property="Padding" Value="0"/>
    <Setter Property="ContentTemplate">
        <Setter.Value>
            <DataTemplate>
                <StackPanel Margin="5"
                            Orientation="{Binding Orientation, RelativeSource={RelativeSource AncestorType=Button}}">
                    <TextBlock Name="IconOff"  
                               FontFamily="Segoe MDL2 Assets"
                               HorizontalAlignment="Center"
                               FontSize="18"
                               Text="{Binding Icon, RelativeSource={RelativeSource AncestorType=Button}}" />
                    <TextBlock Name="TextOff" 
                               Text="{Binding Text, RelativeSource={RelativeSource AncestorType=Button}}"
                               HorizontalAlignment="Center"
                               Margin="0,5,0,0" />
                </StackPanel>
                <DataTemplate.Triggers>
                    <DataTrigger Binding="{Binding State, RelativeSource={RelativeSource AncestorType=Button}}"  Value="True">
                        <Setter TargetName="Text" Property="Text" Value="{Binding TextOn, RelativeSource={RelativeSource AncestorType=Button}}"/>
                    </DataTrigger>
                    <DataTrigger Binding="{Binding State, RelativeSource={RelativeSource AncestorType=Button}}" Value="False">
                        <Setter TargetName="Text" Property="Text" Value="{Binding TextOff, RelativeSource={RelativeSource AncestorType=Button}}"/>
                    </DataTrigger>
                    <DataTrigger Binding="{Binding State, RelativeSource={RelativeSource AncestorType=Button}}"  Value="True">
                        <Setter TargetName="Icon"  Property="Text" Value="{Binding IconOn, RelativeSource={RelativeSource AncestorType=Button}}"/>
                    </DataTrigger>
                    <DataTrigger Binding="{Binding State, RelativeSource={RelativeSource AncestorType=Button}}" Value="False">
                        <Setter TargetName="Icon"  Property="Text" Value="{Binding IconOff, RelativeSource={RelativeSource AncestorType=Button}}"/>
                    </DataTrigger>
                </DataTemplate.Triggers>
            </DataTemplate>
        </Setter.Value>
    </Setter>
</Style>

and an example of use

<local:StateImageButton State="{Binding TheBooleanState}"
                                     TextOff="Start"
                                     TextOn="Stop"
                                     IconOff="&#xE768;"
                                     IconOn="&#xE71A;"
                                     Command="{Binding TheCommand}"/>

To do: Needs a few dependency properties to make it really nice, such as FontSize, FontFamily and Foreground (colour) for both the Icon and Text.

Update 2019-01-04: fixed bug with default values

\$\endgroup\$
2
\$\begingroup\$

General:

Creating specialized controls for each use case was necessary with Windows Forms and it is still possible with WPF. However, WPF has powerful theming, styling and data binding capabilities, that allows to use the existing controls with customized templates / style.

If icon and text are not changing often, I would consider to use the build-in toggle button with 2 customized styles - one for the vertical and one for the horizontal aligned icon / text representation.

Review:

If you need a button where text and icon should be highly customizable, it makes sense to create a new button with additional dependency properties.

Some points that may improve the implemenation:

  • use ToggleButton as base class and replace the "State" property with the exiting (and known by all developers) "IsChecked" property.

  • Instead of setting a "DataTemplate" to the "ContentTemplate" property, I would set a "ControlTemplate" to the "Template" property which is the right way for defining the appearance of a control.

  • use "TemplateBinding" for the wrapped controls, so that fontsize, color, ... can be configured.

    <DataTemplate>
            <StackPanel Margin="5"
                        Orientation="{Binding Orientation, RelativeSource={RelativeSource AncestorType=Button}}">
                <TextBlock Name="IconOff"  
                           FontFamily="{TemplateBinding FontFamily"}
                           HorizontalAlignment="Center"
                           FontSize="{TemplateBinding FontSize, Converter=FontSizeToIconSizeConverter}"
                           Text="{Binding Icon, RelativeSource={RelativeSource AncestorType=Button}}" />
                <TextBlock Name="TextOff" 
                           FontFamily="{TemplateBinding FontFamily"}
                           HorizontalAlignment="Center"
                           FontSize="{TemplateBinding FontSize}"
                           Text="{Binding Text, RelativeSource={RelativeSource AncestorType=Button}}"
                           HorizontalAlignment="Center"
                           Margin="0,5,0,0" />
            </StackPanel>
            ...
    </DataTemplate>
    
  • One Trigger can have mulitple setters, there is no need to define multiple DataTrigger with the same logic:

     <DataTemplate.Triggers>
                <DataTrigger Binding="{Binding State, RelativeSource={RelativeSource AncestorType=Button}}"  Value="True">
                    <Setter TargetName="Text" Property="Text" Value="{Binding TextOn, RelativeSource={RelativeSource AncestorType=Button}}"/>
                    <Setter TargetName="Icon"  Property="Text" Value="{Binding IconOn, RelativeSource={RelativeSource AncestorType=Button}}"/>
                </DataTrigger>
                <DataTrigger Binding="{Binding State, RelativeSource={RelativeSource AncestorType=Button}}" Value="False">
                    <Setter TargetName="Text" Property="Text" Value="{Binding TextOff, RelativeSource={RelativeSource AncestorType=Button}}"/>
                    <Setter TargetName="Icon"  Property="Text" Value="{Binding IconOff, RelativeSource={RelativeSource AncestorType=Button}}"/>
                </DataTrigger>
    </DataTemplate.Triggers>
    

.

\$\endgroup\$
  • \$\begingroup\$ But what about the extra dependancy properties needed? \$\endgroup\$ – geometrikal Jan 6 at 8:46
  • \$\begingroup\$ I have updated my answer \$\endgroup\$ – JanDotNet Jan 6 at 9:26
  • \$\begingroup\$ FontSizeToIconSizeConverter I like it!. For the Content vs Control template, I did it via ContentTemplate so that the inbuilt Button design and behaviours would be used by default. ToggleButton IsActive is a good idea, I guess if I go that way I will have to do a new ControlTemplate to remove the IsActive highlight anyway. \$\endgroup\$ – geometrikal Jan 6 at 9:41
  • \$\begingroup\$ By the way, I'm still not completely sure why I can use TemplateBinding for the default button dependancy properties, but have to use RelativeSource={RelativeSource AncestorType=Button} for the dependency properties in my derived class, can you shed some light? \$\endgroup\$ – geometrikal Jan 6 at 9:42
  • \$\begingroup\$ TemplateBinding can be used within ControlTemplates only. However, your approach with the relative source should also work for font and font size :). \$\endgroup\$ – JanDotNet Jan 6 at 16:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.