【WPF】トグルボタンに表示されている文字の修正を行った際、微修正だと思ったら意外と改修が必要だった【xaml】

# 概要
WPFで作成していたとある画面に対し、改修対応を行いました。改修内容から判断して一瞬で終わるかと思いきや、意外と時間が掛かったので備忘録として残します。

# 仕様や対応内容

### 改修前
元々、以下のような画面を作成していました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3533049/8a3c3c6f-0316-dee9-b947-9c831bbc5be7.png" width="500">

ボタン1とボタン2はトグルボタンで、押下状態と未押下状態でボタンの色やボタンの文字の色が変わります。ボタン1とボタン2は画面を起動する条件によって、活性状態になったり非活性状態になったりすることもあります。

### 改修後
上記の画面から、以下のような画面に改修を行いました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3533049/5404d8ab-2eec-ff5f-cd52-5bfb153325f3.png" width="500">

変更としては、ボタン2の文字の下に、小さく注釈を記載するのみでした。
ボタンの文言と、文字のサイズの変更だけだったのでそれほど時間が掛からないと思っていましたが、思っていたより時間が掛かりました。

# 対応前のコード

### View
- `SampleView.xaml`に`ToggleButton`でボタン1とボタン2を定義しています
- `ToggleButton`は`ResourceDictonary.xaml`で定義したテンプレートを使用しています

```xml:SampleView.xaml
<Window
    x:Class="HogeNamespace.SampleView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="サンプル画面"
    Width="530"
    Height="240"
    ResizeMode="NoResize">
    <Window.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="../StyleDictionary.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Window.Resources>
    <Grid>
        <StackPanel>
            <!--  トグルボタン  -->
            <StackPanel Margin="0,10,0,0" Orientation="Horizontal">
                <StackPanel Width="260" Margin="30,20,0,0">
                    <ToggleButton
                        Width="180"
                        Height="70"
                        HorizontalAlignment="Left"
                        Command="{Binding LeftButtonCommand}"
                        FontSize="15"
                        Content="ボタン1"
                        IsChecked="{Binding IsCheckedLeftButton}"
                        Template="{StaticResource SampleViewButton}" />
                </StackPanel>
                <StackPanel Width="320" Margin="0,20,0,0">
                    <ToggleButton
                        Width="180"
                        Height="70"
                        HorizontalAlignment="Left"
                        Command="{Binding RightButtonCommand}"
                        FontSize="15"
                        Content="ボタン2"
                        IsChecked="{Binding IsCheckedRightButton}"
                        Template="{StaticResource SampleViewButton}" />
                </StackPanel>
            </StackPanel>
            <Border
                Width="450"
                Height="1"
                Margin="0,30,0,0"
                BorderBrush="LightGray"
                BorderThickness="0,10,0,0" />
            <!--  OK、キャンセルボタン  -->
            <StackPanel
                Margin="30,20,0,0"
                HorizontalAlignment="Left"
                Orientation="Horizontal">
                <TextBlock Text="いずれかのボタンを選択して下さい"
                           Margin="0,0,70,0"
                           FontSize="15"/>
                <StackPanel Margin="0,0,15,0" HorizontalAlignment="Right">
                    <Button
                        Width="90"
                        Height="20"
                        HorizontalContentAlignment="Center"
                        VerticalContentAlignment="Center"
                        Command="{Binding OKCommand}"
                        Content="OK"/>
                </StackPanel>
                <StackPanel Margin="0,0,15,0" HorizontalAlignment="Right">
                    <Button
                        Width="90"
                        Height="20"
                        HorizontalContentAlignment="Center"
                        VerticalContentAlignment="Center"
                        Command="{Binding CancelCommand}"
                        Content="Cancel"/>
                </StackPanel>
            </StackPanel>
        </StackPanel>
    </Grid>
</Window>

```

### ResourceDictonary
- `SampleViewButton`という名前で、`ControlTemplate`を定義しています
- `TextBlock`の`Text`は`{TemplateBinding Content}`という形で、テンプレートを使用するコントロールの`Content`をバインディングするようにしていました
- `MultiDataTrigger`を使用して、ボタンの押下状態や活性状態の組み合わせで、ボタンの色やボタンの表示名の色を変更していました

```xml:StyleDictionary.xaml
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <ControlTemplate x:Key="SampleViewButton" TargetType="{x:Type ToggleButton}">
        <Border x:Name="border" CornerRadius="5">
            <TextBlock
                x:Name="text"
                HorizontalAlignment="Center"
                VerticalAlignment="Center"
                FontSize="15"
                FontWeight="Bold"
                Text="{TemplateBinding Content}" />
        </Border>
        <ControlTemplate.Triggers>
            <MultiDataTrigger>
                <MultiDataTrigger.Conditions>
                    <Condition Binding="{Binding IsChecked, RelativeSource={RelativeSource Self}}" Value="False" />
                    <Condition Binding="{Binding IsEnabled, RelativeSource={RelativeSource Self}}" Value="False" />
                </MultiDataTrigger.Conditions>
                <Setter TargetName="border" Property="Background" Value="Gainsboro" />
                <Setter TargetName="border" Property="BorderBrush" Value="Transparent" />
                <Setter TargetName="text" Property="Foreground" Value="White" />
            </MultiDataTrigger>
            <MultiDataTrigger>
                <MultiDataTrigger.Conditions>
                    <Condition Binding="{Binding IsChecked, RelativeSource={RelativeSource Self}}" Value="False" />
                    <Condition Binding="{Binding IsEnabled, RelativeSource={RelativeSource Self}}" Value="True" />
                </MultiDataTrigger.Conditions>
                <Setter TargetName="border" Property="Background" Value="DarkGray" />
                <Setter TargetName="border" Property="BorderBrush" Value="Transparent" />
                <Setter TargetName="text" Property="Foreground" Value="White" />
            </MultiDataTrigger>
            <MultiDataTrigger>
                <MultiDataTrigger.Conditions>
                    <Condition Binding="{Binding IsChecked, RelativeSource={RelativeSource Self}}" Value="True" />
                    <Condition Binding="{Binding IsEnabled, RelativeSource={RelativeSource Self}}" Value="True" />
                </MultiDataTrigger.Conditions>
                <Setter TargetName="border" Property="Background" Value="DodgerBlue" />
                <Setter TargetName="border" Property="BorderBrush" Value="Transparent" />
                <Setter TargetName="text" Property="Foreground" Value="Black" />
            </MultiDataTrigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>
</ResourceDictionary>
```

# 対応後のコード
### View
- `ToggleButton`の`Content`を消去し、`TextBlock`を直接編集する形にしました
- `TextBlock`は`ResourceDictionary`で定義した`Style`を使用する形にしています

```xml:SampleView.xaml
<Window
    x:Class="HogeNamespace.SampleView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="サンプル画面"
    Width="530"
    Height="240"
    ResizeMode="NoResize">
    <Window.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="../StyleDictionary.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Window.Resources>
    <Grid>
        <StackPanel>
            <!--  トグルボタン  -->
            <StackPanel Margin="0,10,0,0" Orientation="Horizontal">
                <StackPanel Width="260" Margin="30,20,0,0">
                    <ToggleButton
                        Width="180"
                        Height="70"
                        HorizontalAlignment="Left"
                        Command="{Binding LeftButtonCommand}"
                        IsChecked="{Binding IsCheckedLeftButton}"
                        Template="{StaticResource SampleViewButton}" >
                        <ToggleButton.Content>
                            <StackPanel>
                                <TextBlock
                                    FontWeight="Bold"
                                    Style="{StaticResource SampleViewButtonTextBlockStyle}">
                                    <Run FontSize="15" FontWeight="Bold">ボタン1</Run>
                                </TextBlock>
                            </StackPanel>
                        </ToggleButton.Content>
                    </ToggleButton>
                </StackPanel>
                <StackPanel Width="320" Margin="0,20,0,0">
                    <ToggleButton
                        Width="180"
                        Height="70"
                        HorizontalAlignment="Left"
                        Command="{Binding RightButtonCommand}"
                        IsChecked="{Binding IsCheckedRightButton}"
                        Template="{StaticResource SampleViewButton}" >
                        <ToggleButton.Content>
                            <StackPanel HorizontalAlignment="Left">
                                <TextBlock TextAlignment="Center" Style="{StaticResource SampleViewButtonTextBlockStyle}">
                                    <Run FontSize="15" FontWeight="Bold">ボタン2</Run>
                                    <LineBreak/>
                                    <Run FontSize="10">※注釈</Run>
                                </TextBlock>
                            </StackPanel>
                        </ToggleButton.Content>
                    </ToggleButton>
                </StackPanel>
            </StackPanel>
            <Border
                Width="450"
                Height="1"
                Margin="0,30,0,0"
                BorderBrush="LightGray"
                BorderThickness="0,10,0,0" />
            <!--  OK、キャンセルボタン  -->
            <StackPanel
                Margin="30,20,0,0"
                HorizontalAlignment="Left"
                Orientation="Horizontal">
                <TextBlock Text="いずれかのボタンを選択して下さい"
                           Margin="0,0,70,0"
                           FontSize="15"/>
                <StackPanel Margin="0,0,15,0" HorizontalAlignment="Right">
                    <Button
                        Width="90"
                        Height="20"
                        HorizontalContentAlignment="Center"
                        VerticalContentAlignment="Center"
                        Command="{Binding OKCommand}"
                        Content="OK"/>
                </StackPanel>
                <StackPanel Margin="0,0,15,0" HorizontalAlignment="Right">
                    <Button
                        Width="90"
                        Height="20"
                        HorizontalContentAlignment="Center"
                        VerticalContentAlignment="Center"
                        Command="{Binding CancelCommand}"
                        Content="Cancel"/>
                </StackPanel>
            </StackPanel>
        </StackPanel>
    </Grid>
</Window>

```

### ResourceDictonary
- `ControlTemplate`の`TextBlock`部分を`ContentPresenter`に変更しました
- また、ボタンの表示名の`TextBlock`部分は新しく`Style`として別に定義しました

```xml:StyleDictionary.xaml
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <ControlTemplate x:Key="SampleViewButton" TargetType="{x:Type ToggleButton}">
        <Border x:Name="border" CornerRadius="5">
            <ContentPresenter
                HorizontalAlignment="Center"
                VerticalAlignment="Center"
                Content="{TemplateBinding Content}"/>
        </Border>
        <ControlTemplate.Triggers>
            <MultiDataTrigger>
                <MultiDataTrigger.Conditions>
                    <Condition Binding="{Binding IsChecked, RelativeSource={RelativeSource Self}}" Value="False" />
                    <Condition Binding="{Binding IsEnabled, RelativeSource={RelativeSource Self}}" Value="False" />
                </MultiDataTrigger.Conditions>
                <Setter TargetName="border" Property="Background" Value="Gainsboro" />
                <Setter TargetName="border" Property="BorderBrush" Value="Transparent" />
            </MultiDataTrigger>
            <MultiDataTrigger>
                <MultiDataTrigger.Conditions>
                    <Condition Binding="{Binding IsChecked, RelativeSource={RelativeSource Self}}" Value="False" />
                    <Condition Binding="{Binding IsEnabled, RelativeSource={RelativeSource Self}}" Value="True" />
                </MultiDataTrigger.Conditions>
                <Setter TargetName="border" Property="Background" Value="DarkGray" />
                <Setter TargetName="border" Property="BorderBrush" Value="Transparent" />
            </MultiDataTrigger>
            <MultiDataTrigger>
                <MultiDataTrigger.Conditions>
                    <Condition Binding="{Binding IsChecked, RelativeSource={RelativeSource Self}}" Value="True" />
                    <Condition Binding="{Binding IsEnabled, RelativeSource={RelativeSource Self}}" Value="True" />
                </MultiDataTrigger.Conditions>
                <Setter TargetName="border" Property="Background" Value="DodgerBlue" />
                <Setter TargetName="border" Property="BorderBrush" Value="Transparent" />
            </MultiDataTrigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>
    
    <Style x:Key="SampleViewButtonTextBlockStyle" TargetType="{x:Type TextBlock}">
        <Setter Property="TextAlignment" Value="Center" />
        <Style.Triggers>
            <MultiDataTrigger>
                <MultiDataTrigger.Conditions>
                    <Condition Binding="{Binding IsChecked, RelativeSource={RelativeSource AncestorType=ToggleButton}}" Value="False" />
                    <Condition Binding="{Binding IsEnabled, RelativeSource={RelativeSource AncestorType=ToggleButton}}" Value="False" />
                </MultiDataTrigger.Conditions>
                <Setter  Property="Foreground" Value="White" />
            </MultiDataTrigger>
            <MultiDataTrigger>
                <MultiDataTrigger.Conditions>
                    <Condition Binding="{Binding IsChecked, RelativeSource={RelativeSource AncestorType=ToggleButton}}" Value="False" />
                    <Condition Binding="{Binding IsEnabled, RelativeSource={RelativeSource AncestorType=ToggleButton}}" Value="True" />
                </MultiDataTrigger.Conditions>
                <Setter Property="Foreground" Value="White" />
            </MultiDataTrigger>
            <MultiDataTrigger>
                <MultiDataTrigger.Conditions>
                    <Condition Binding="{Binding IsChecked, RelativeSource={RelativeSource AncestorType=ToggleButton}}" Value="True" />
                    <Condition Binding="{Binding IsEnabled, RelativeSource={RelativeSource AncestorType=ToggleButton}}" Value="True" />
                </MultiDataTrigger.Conditions>
                <Setter Property="Foreground" Value="Black" />
            </MultiDataTrigger>
        </Style.Triggers>
    </Style>
</ResourceDictionary>
```

# 時間が掛かった対応箇所
そもそも改修前の対応で、xamlの`ToggleButton`の`Content`にボタンの表示名 (「ボタン1」や「ボタン2」) を記載していました。ただ`Content`だと、`Content`の中の特定の文字だけ形式を変更することはできません。
そのため、`ToggleButton`中の`TextBlock`を直接書き換え`<Run>`を使用する形でボタンの表示名を定義する方針にしました。その対応でボタン内の表示名の特定の文字だけ形式を変えることはできました。

ここから少し時間が掛かったのですが、元々ボタン1と2は`ResourceDictonary`に`ControlTemplate`定義し、そのテンプレートを使用していました。ただ`ControlTemplate`に定義していたボタンの`TextBlock`で、Text="{TemplateBinding Content}"という記述を使用していましたが、今回の対応で`ToggleButton`の`Content`が`TextBlock`でラップされているため、単純な文字列として扱えないためか、文字がバインディングされず表示されなくなっていました。
そのため、`ControlTemplate`の`TextBlock`部分は`ControlTemplate`内で任意のコンテンツをレンダリングできる、`ContentPresenter`に変更しました。また、`ResourceDictonary`で定義する部分も`ToggleButton`と`TextBlock`部分で分けることで、従来通り`ResourceDictonary`の定義を参照する形で対応することができました。

`ToggleButton`の`TextBlock`の`Style`を別途切り出した理由は、`ControlTemplate`の`SampleViewButton`の中にある状態だと、今回の場合は`TargetName`を使用しないとプロパティを変更するターゲットコントロールが指定できなかったためです。
`ContentPresenter`に`x:Name`を指定して、SetterからTargetNameを指定してもうまくいきませんでした。
なので、`ToggleButton`の中の`TextBlock`に直接適用できる`Style`を作成し、`TargetName`を使用せずともプロパティをセットできる形にしました。

# おわりに
今回時間が掛かった理由は、`ToggleButton`の`Content`を消去したため、それに伴う`ResourceDictonary`の改修が少し多くなった点でした。
もっとよりシンプルに改修できる方法があれば知りたいです。