sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

WPF RadioButton と Enum 値の Binding

WPF の RadioButton の IsChecked プロパティに Enum 値を Binding させたいシーンが結構よくあります。地味に使うケースがあるんだけど、記述方法にひと手間あるんでその内容を記録しました。

この記事の内容はもしかすると、過去にも書いていてもおかしくありません……が、検索しても(自分の記事には)出てこなかったので改めてメモすることにしています。

重複していたら悔しいので CommunityToolkit.Mvvm を利用しています。

XAML View

ポイントは、Enum の値を Binding するために Converter を用意している点だけです。

Converter はConverterParameter で Binding の値に相当する文字列を手がかりにして、RadioButton との Binding を成立させます。

ConverterParameter の文字列を記入する部分は、インテリセンスの効かない範囲なので、入力ミスをするとわかりづらいバグに繋がる恐れがあります。nameof() みたいなことができるといいんだけど xaml は、かゆいところがあると思います。

<Window x:Class="RadioEnumBindingTest.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:local="clr-namespace:RadioEnumBindingTest"
        mc:Ignorable="d"
        d:DataContext="{d:DesignInstance local:MainWindowViewModel}"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:MainWindowViewModel x:Name="ViewModel" />
    </Window.DataContext>
    <Window.Resources>
        <local:BoolToEnumConverter x:Key="BoolToEnumConverter" />
    </Window.Resources>
    <StackPanel Orientation="Vertical">
        <RadioButton Margin="10" 
                     GroupName="test" 
                     IsChecked="{Binding Sample, ConverterParameter=A, Converter={StaticResource BoolToEnumConverter}}"
                     Content="A radio" />
        <RadioButton Margin="10" 
                     GroupName="test" 
                     IsChecked="{Binding Sample, ConverterParameter=B, Converter={StaticResource BoolToEnumConverter}}" 
                     Content="B radio" />
        <RadioButton Margin="10" 
                     GroupName="test" 
                     IsChecked="{Binding Sample, ConverterParameter=C, Converter={StaticResource BoolToEnumConverter}}" 
                     Content="C radio" />

        <TextBlock Margin="10" Text="{Binding Text, StringFormat={}selected radio: {0}}" />
    </StackPanel>
</Window>

ViewModel

ViewModel は素直に自分で binding プロパティを用意してもいいけど、めんどくさいので MVVM Toolkit を使って用意します。

MVVM ToolKit は(動的な)ソースジェネレータってすごいっていうのを実感できるので、一度は使ってみるといいと思います。

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace RadioEnumBindingTest;

public partial class MainWindowViewModel : ObservableObject
{
    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(Text))]
    private SampleEnum _sample = SampleEnum.A;

    public string Text => $"{Sample}.";

    /// <summary>
    /// <see cref="MainWindowViewModel"/> クラスの新しいインスタンスを初期化します。
    /// </summary>
    public MainWindowViewModel()
    {
    }
}

NotifyPropertyChangedFor は、わかりやすい通知のサンプルになっていると思います。

このほかのやり方として、オーバーロードを実装して Sample が変更されたとき/される前に Text を更新する、という方法もとれると思います。こっちのほうが oldvalue/newvalue を引数に持てたりもするから、通知ロジックとして強力。

partical void OnSampleChanged(SampleEnum value)
{
    Console.WriteLine($"Enum value is about to changed to {value}.");
}

Converter

IValueConverter は今更といった感じですが。Enum の比較方法はいろいろなやり方があると思います。手を抜くなら、文字列にして比較するだけでも(大体は)いいし。

わかりやすく比較していくと、下例のようになりました。

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;

namespace RadioEnumBindingTest;

public class BoolToEnumConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value == null || parameter == null)
        {
            return DependencyProperty.UnsetValue;
        }

        if (Enum.IsDefined(value.GetType(), value) == false)
        {
            return DependencyProperty.UnsetValue;
        }

        var isResult = false;
        var canParse = Enum.TryParse(value.GetType(), parameter.ToString(), out object? result);

        if (canParse && result != null)
        {
            isResult = (int)result == (int)value;
        }
        else
        {
            isResult = false;
        }

        return isResult;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var parameterText = parameter?.ToString() ?? "";

        if (parameterText == "")
        {
            return DependencyProperty.UnsetValue;
        }

        return Enum.Parse(targetType, parameterText);
    }
}

サンプル

今回のコードを公開しておきます。

参考

なし