こんにちは、働くC#プログラマーのさんさめです。
WPFでアプリ開発していると、
よくやりたくなってくることの1つに、
「ある条件がtrue(またはfalse)のときだけ、
画面上に表示したいUI」
というものがあります。
たとえば
「通常は選択肢から入力して欲しいが、
チェックを入れたら自由入力を可能にする」
といった仕様です。

しかし、コントロールの可視を定める
Visibilityプロパティは、
列挙型でありboolではありません。
そのため、
「ViewModelでboolのプロパティを、
ViewでBindingしてVisibilityに使いたい」
と思っても素直にはBindingできないということになります。

ということは、
ViewModelにVisibilityを返すプロパティを
用意すればいいってこと?
それも一案ですが、
1つ1つViewModelに専用のプロパティを用意するのは、
割と…いえ、かなり面倒な作業です。
WPFには、もっと簡単な手があります。
それは、Bindingの機能の一つである、
コンバーター(Converter)を使う方法です。
この方法ですと、ViewModelに余計なコードを足す必要がなく、
xamlだけで完結することができます。
さっそく解説していきます。
既定で用意されているBooleanToVisibilityConverterを使う方法
実は、
そのものずばりなコンバーターが標準で用意されています。
それが、BooleanToVisibilityConverterです。
これは、Bindingしたboolの値が
trueならVisible、falseならCollapseに
変換してくれるコンバーターです。
使うための手順は次のようになります。
- 使いたいWindowやUserControlの
ResourcesにBooleanToVisibilityConverterを含めておく - boolをBindingしたい箇所で、
1.で作ったConvereterを指定する
以下にサンプルコードを示します。
<Window x:Class="BoolToVisibleConverterSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="160" Width="240">
<!-- インスタンスを作ってResourcesに含めておく -->
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVisible"/>
</Window.Resources>
<StackPanel Margin="4">
<TextBlock Text="年代を選んでください"/>
<WrapPanel>
<RadioButton Content="20代" IsChecked="True"/>
<RadioButton Content="30代" Margin="8 0 0 0"/>
<RadioButton Content="40代" Margin="8 0 0 0"/>
<RadioButton Content="その他" Margin="8 0 0 0"
IsChecked="{Binding IsOther}"/>
</WrapPanel>
<!-- Resourceの中にあるBooleanToVisibilityConverterを使う -->
<StackPanel Margin="12 0 0 0"
Visibility="{Binding IsOther, Converter={StaticResource BoolToVisible}}">
<TextBlock Text="その他の場合は年齢を入力してください" Margin="0 4 0 0"/>
<TextBox />
</StackPanel>
</StackPanel>
</Window>
ViewModelにはIsOtherというboolのプロパティが
あると思ってください。
これを実行してみると、
以下のようになります。
(gifアニメです)

「その他」を選んだ時だけ、
UIが表示されるようになりました。
めでたしめでたし…。

…実はこのサンプルコード、
まだ無駄があります
どういうことでしょうか?
実は私、冒頭で
「ViewModelでboolのプロパティに…」
と言っていたため、
一旦ViewModelのプロパティを介してBindingする
というコードをあえて書いていました。
ただ、この場合本当はViewModelを介する必要はありません。
ElementNameを使って、
直接RadioButtonのIsCheckedプロパティに対して
バインディングをしてあげれば良いのです。
修正したコードが以下になります。
<Window x:Class="BoolToVisibleConverterSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="160" Width="240">
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVisible"/>
</Window.Resources>
<StackPanel Margin="4">
<TextBlock Text="年代を選んでください"/>
<WrapPanel>
<RadioButton Content="20代" IsChecked="True"/>
<RadioButton Content="30代" Margin="8 0 0 0"/>
<RadioButton Content="40代" Margin="8 0 0 0"/>
<!-- IsCheckedをBindingする代わりに名前を付ける -->
<RadioButton Content="その他" Margin="8 0 0 0"
x:Name="OtherRadioButton"/>
</WrapPanel>
<!-- ElementNameで直接RadioButtonのプロパティにBindingする -->
<StackPanel Margin="12 0 0 0"
Visibility="{Binding IsChecked, ElementName=OtherRadioButton, Converter={StaticResource BoolToVisible}}">
<TextBlock Text="その他の場合は年齢を入力してください" Margin="0 4 0 0"/>
<TextBox />
</StackPanel>
</StackPanel>
</Window>
Bindingのところは記述量が増えていますが、
逆にViewModelの方には
余計なプロパティを生やす必要が無くなりました。
Viewで完結できる部分は、
積極的にViewだけに留めておくと、
コードの保守性を高めることができます。

ちょっと待って、
何が起きてるか全然分からなくなっちゃったんだけど!
たしかにちょっと難解ですね。
でも、1つずつ理解していけば
実はそれほど難しくありません。
「ElementNameってなんやねん!」
という方は、
【WPF】Binding入門2。Binding対象を変更するには
で詳しく解説しているので、ぜひご覧ください。
それ以前に
「もう、{}が何回も出てくるとお手上げ…」
という方は、
【WPF】読める!xamlマークアップ拡張【入れ子でも怖くない】
がおススメです。
…すこし話題が逸れてしまいました。
ところで、このConverterは、
trueの時にVisibleになるというものでした。
しかし、実際の開発現場では、
falseの時にVisibleになって欲しいこともよくあります。
この場合はコンバーターを自作するしかありません。
というわけで、作ってみました。
boolを任意のVisibilityに変換するコンバータを作ってみた
bool値を任意のVisibilityに変換する
コンバータを作ってみました。
trueの時とfalseの時それぞれに、
Visibility列挙値を指定することができます。
public class BoolToAnyVisibilityConverter : IValueConverter
{
public Visibility? TrueTo { get; set; }
public Visibility? FalseTo { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (!TrueTo.HasValue)
{
throw new InvalidOperationException($"{nameof(TrueTo)}にVisibilityが設定されていません");
}
if (!FalseTo.HasValue)
{
throw new InvalidOperationException($"{nameof(FalseTo)}にVisibilityが設定されていません");
}
if (!(value is bool b)) { return DependencyProperty.UnsetValue; }
return b ? TrueTo : FalseTo;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
使い方は以下のようになります。
<local:BoolToAnyVisibilityConverter x:Key="BoolToCollapse"
TrueTo="Collapsed" FalseTo="Visible"/>
これを先ほどのサンプルコードで使うようにしてみると、
以下のように挙動が逆になります。
(再びgifアニメです)

ちなみに、TrueToかFalseToのどちらかを、
設定し忘れている状態で使おうとすると、
xaml上で警告が表示されます。

これで、
- trueの時だけ隠したい
- falseの時にはCollapsedじゃなくてHiddenがいい
といったケースに柔軟に対応できるでしょう。
あなたのWPF開発が
より円滑に進むことを祈っています。
ともに頑張りましょうね。
まとめ
まとめです
- boolをVisibilityにバインディングしたい時はコンバータを使おう
- trueの時だけ可視化したいなら
既定のコンバータも使える - より柔軟に設定したいなら
コンバータを自作しよう(コード公開中)
最後までお読みいただきありがとうございました。
関連記事
今回の記事はBindingのConverterプロパティを使う話でした。
Bindingの記述がおまじない過ぎて読めないという方は、
【WPF】読める!xamlマークアップ拡張【入れ子でも怖くない】
を読むと、実は簡単だったことが分かります。
コメント