0

I have a WPF style for a toggle button that uses a stack panel to achieve stacked, vertical text. I want the button text to change based on the toggle button's IsChecked state.

Additionally, the number of characters changes so I need to hide one of the text blocks in the stack panel. I tried setting the Visibility property of the Letter4 text block to hidden but the text was not vertically centered.

The code below works but it's just a cheesy workaround — I change the font size to 1 so it seems to disappear. (I pulled out all the formatting to make it simpler.) What is the correct way to do what I need?

Thanks.

<Style x:Key="RunStopToggle" TargetType="ToggleButton">

    <Setter Property="Template">
        <Setter.Value>

            <ControlTemplate TargetType="ToggleButton">

                    <StackPanel VerticalAlignment="Center">
                        <TextBlock x:Name="Letter1"/>                                                                                                                         
                        <TextBlock x:Name="Letter2"/>                                                                                                                          
                        <TextBlock x:Name="Letter3"/>                                                                                             
                        <TextBlock x:Name="Letter4"/>                                                                                              
                    </StackPanel>

                <ControlTemplate.Triggers>

                    <Trigger Property="IsChecked" Value="True">
                        <Setter Property="TextBlock.Text" TargetName="Letter1" Value="S"/>
                        <Setter Property="TextBlock.Text" TargetName="Letter2" Value="T"/>
                        <Setter Property="TextBlock.Text" TargetName="Letter3" Value="O"/>
                        <Setter Property="TextBlock.Text" TargetName="Letter4" Value="P"/>
                    </Trigger>

                    <Trigger Property="IsChecked" Value="False">
                        <Setter Property="TextBlock.Text" TargetName="Letter1" Value="R"/>
                        <Setter Property="TextBlock.Text" TargetName="Letter2" Value="U"/>
                        <Setter Property="TextBlock.Text" TargetName="Letter3" Value="N"/>
                        <Setter Property="TextBlock.Text" TargetName="Letter4" Value=""/>
                        <Setter Property="TextBlock.FontSize" TargetName="Letter4" Value="1"/>
                    </Trigger>

                </ControlTemplate.Triggers>

            </ControlTemplate>
            
        </Setter.Value>
    </Setter>
    
</Style>
RaymondC
  • 13
  • 3

2 Answers2

0

IMHO, the biggest problem with the code is that you're trying to do everything in XAML instead of letting a view model mediate the changeable values. A smaller issue is how you're actually implementing the vertically-stacked text.

There is already another question with good advice about vertically-stacked text. See Vertical Text in Wpf TextBlock

We can combine the advice there, where they use ItemsControl to display the text vertically, along with a view model to provide the actual text, and a placeholder ItemsControl that is hidden, but not collapsed (so that it still takes up space), to display the toggle button much more simply than in the code you have now.

First, the view model:

class ViewModel : INotifyPropertyChanged
{

    private bool _isChecked;
    public bool IsChecked
    {
        get => _isChecked;
        set => _UpdateField(ref _isChecked, value, _OnIsCheckedChanged);
    }

    private string _buttonText;
    public string ButtonText
    {
        get => _buttonText;
        set => _UpdateField(ref _buttonText, value);
    }

    public ViewModel()
    {
        ButtonText = _GetTextForButtonState();
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void _OnIsCheckedChanged(bool previous)
    {
        ButtonText = _GetTextForButtonState();
    }

    private string _GetTextForButtonState()
    {
        return IsChecked ? "STOP" : "RUN";
    }

    private void _UpdateField<T>(ref T field, T newValue,
        Action<T> onChangedCallback = null,
        [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, newValue))
        {
            return;
        }

        T oldValue = field;

        field = newValue;
        onChangedCallback?.Invoke(oldValue);
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

This view model just provides a property to receive the toggle button's state, as well as to provide the appropriate button text for that state.

Next, the XAML to use this view model:

<Window x:Class="TestSO68091382ToggleVerticalText.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:l="clr-namespace:TestSO68091382ToggleVerticalText"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
  <Window.DataContext>
    <l:ViewModel/>
  </Window.DataContext>
  
  <Grid>
    <ToggleButton IsChecked="{Binding IsChecked}"
                  HorizontalAlignment="Left" VerticalAlignment="Top">
      <ToggleButton.Content>
        <Grid>
          <ItemsControl ItemsSource="STOP" Visibility="Hidden"/>
          <ItemsControl ItemsSource="{Binding ButtonText}" VerticalAlignment="Center"/>
        </Grid>
      </ToggleButton.Content>
    </ToggleButton>
  </Grid>
</Window>

The ToggleButton.IsChecked property is bound to the IsChecked property in the view model, so that it can update the text as necessary. Then the content of the ToggleButton includes the ItemsControl that will display the text vertically.

Note that button's direct descendent is actually a Grid. This is so that two different ItemsControl elements can be provided: one shows the text itself, and is bound to the ButtonText property; the other has hard-coded the longer of the two strings that might be displayed. This ensures that the ToggleButton's size is always the same, large enough for that longer text. The bound ItemsControl is then centered vertically; you can of course use whatever aligment you like there, but the way your question is worded it sounds like you want the text vertically centered.


For what it's worth, if you really want to do everything in XAML, that's possible to. I personally prefer to avoid this kind of use for triggers, but I admit there's no hard and fast rule that says you can't. My preference mainly has to do with my desire to keep the XAML as simple as possible, as I find it a less readable language, and harder to mentally keep track of all the different related elements, which adding triggers tends to make more complex.

If you do prefer a XAML-only solution, it would look like this:

<Window x:Class="TestSO68091382ToggleVerticalText.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:l="clr-namespace:TestSO68091382ToggleVerticalText"
        xmlns:s="clr-namespace:System;assembly=netstandard"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
  <Window.Resources>
    <s:String x:Key="runText">RUN</s:String>
    <s:String x:Key="stopText">STOP</s:String>
  </Window.Resources>

  <Grid>
    <ToggleButton HorizontalAlignment="Left" VerticalAlignment="Top">
      <ToggleButton.Content>
        <Grid>
          <ItemsControl ItemsSource="STOP" Visibility="Hidden"/>
          <ItemsControl VerticalAlignment="Center">
            <ItemsControl.Style>
              <Style TargetType="ItemsControl">
                <Setter Property="ItemsSource" Value="{StaticResource runText}"/>
                <Style.Triggers>
                  <DataTrigger Binding="{Binding IsChecked, RelativeSource={RelativeSource AncestorType=ToggleButton}}" Value="True">
                    <Setter Property="ItemsSource" Value="{StaticResource stopText}"/>
                  </DataTrigger>
                </Style.Triggers>
              </Style>
            </ItemsControl.Style>
          </ItemsControl>
        </Grid>
      </ToggleButton.Content>
    </ToggleButton>
  </Grid>
</Window>

Mechanically, this is very similar to the view model-based example above, just using a DataTrigger to respond to the changes in the ToggleButton.IsChecked state instead of doing it in the view model.

Note that you really only need one trigger. You can use a Setter to provide the unchecked state's value, and then use a single trigger to override that value for the checked state.

Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
  • Thank you, Peter, for the detailed answer. To be honest though, my skill level is not yet at the point where I fully understand what you wrote. I copied your solution to my notes and hope to implement it in the future. – RaymondC Jun 23 '21 at 15:46
-1

You need to change Visibility:

    <Style x:Key="RunStopToggle" TargetType="ToggleButton">

        <Setter Property="Template">
            <Setter.Value>

                <ControlTemplate TargetType="ToggleButton">

                    <StackPanel VerticalAlignment="Center">
                        <TextBlock x:Name="Letter1"/>
                        <TextBlock x:Name="Letter2"/>
                        <TextBlock x:Name="Letter3"/>
                        <TextBlock x:Name="Letter4" Text="P"/>
                    </StackPanel>

                    <ControlTemplate.Triggers>

                        <Trigger Property="IsChecked" Value="True">
                            <Setter Property="TextBlock.Text" TargetName="Letter1" Value="S"/>
                            <Setter Property="TextBlock.Text" TargetName="Letter2" Value="T"/>
                            <Setter Property="TextBlock.Text" TargetName="Letter3" Value="O"/>
                        </Trigger>

                        <Trigger Property="IsChecked" Value="False">
                            <Setter Property="TextBlock.Text" TargetName="Letter1" Value="R"/>
                            <Setter Property="TextBlock.Text" TargetName="Letter2" Value="U"/>
                            <Setter Property="TextBlock.Text" TargetName="Letter3" Value="N"/>
                            <Setter Property="TextBlock.Visibility" TargetName="Letter4" Value="Collapsed"/>
                        </Trigger>

                    </ControlTemplate.Triggers>

                </ControlTemplate>

            </Setter.Value>
        </Setter>

    </Style>

But I agree with @Peter Duniho - you'd better use a different approach for vertical text.

For permanent text, to print it one letter on each line, it is enough to insert newline characters & # xA; between the letters.
Example:

    <Style x:Key="RunStopToggle" TargetType="ToggleButton">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="ToggleButton">

                    <StackPanel VerticalAlignment="Center">
                            <TextBlock x:Name="PART_TextBlock" Text="R&#xA;U&#xA;N"/>
                    </StackPanel>

                    <ControlTemplate.Triggers>

                        <Trigger Property="IsChecked" Value="True">
                                <Setter Property="TextBlock.Text" TargetName="PART_TextBlock" Value="S&#xA;T&#xA;O&#xA;P"/>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

Also see my answer with a converter for vertical text: https://stackoverflow.com/a/68094601/13349759

EldHasp
  • 6,079
  • 2
  • 9
  • 24
  • I had tried setting the Visibility to Hidden but the text was not centered. I hadn't tried Collapsed, that solved it. But I also like the tip. Quick and easy. Thank you. – RaymondC Jun 23 '21 at 15:52