🤮

【WPF】静的プロパティをバインディングさせているクラスに迂闊にイベント追加すると実行時例外を吐く

2023/07/25に公開

開発でハマった小ネタです。
WPFのバインディングは、基本的にインスタンスプロパティを対象にしたものです。静的プロパティの場合には以下のようにStaticPropertyChangedというイベントを定義してやることでバインディング可能になります。このイベントのシグネチャは完全にEventHandler<PropertyChangedEventArgs>型で決め打ち(当然リフレクションで)参照されるので、この通りに定義する必要があります(前振り)。

static class Context
{
    static int count = 100;

    public static int Count
    {
        get => count;
        set
        {
            count = value;
            StaticPropertyChanged?.Invoke(null, new(nameof(Count)));
        }
    }

    public static event EventHandler<PropertyChangedEventArgs>? StaticPropertyChanged;
}
<Window x:Class="WpfApp10.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApp10"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel>
        <Button Content="{Binding Path=(local:Context.Count)}" Click="OnClick"/>
    </StackPanel>
</Window>
public partial class MainWindow : Window
{
    public MainWindow() => InitializeComponent();

    void OnClick(object sender, RoutedEventArgs e) => Context.Count++;
}

さて、上記のように定義したイベントをほかのコードからも購読しようとするとStaticPropertyChangedなるイベントを呼び出すことになりますが、あまり良くはない命名のイベントです。というわけでCountChangedなるイベントを追加します。静的プロパティなので、EventHandler第一引数のsenderは不要です。なのでシンプルにAction型で定義しましょう。

static class Context
{
    static int count = 100;

    public static int Count
    {
        get => count;
        set
        {
            count = value;
	    CountChanged?.Invoke(count);
            StaticPropertyChanged?.Invoke(null, new(nameof(Count)));
        }
    }

    public static event EventHandler<PropertyChangedEventArgs>? StaticPropertyChanged;
    public static event Action<int>? CountChanged;
}

さてこれを実行すると、「想定してないシグネチャのデリゲートがある」と言われて怒られてしまいます。

実は静的プロパティバインディングの際には、「プロパティ名+"Changed"」という名前のイベントがあったらそいつをリフレクションで呼び出すという仕様にもなっています。そしてこのイベントも型はEventHandler<PropertyChangedEventArgs>であるはずと想定しているのです。なのでシグネチャ不一致で実行時例外となってしまうワケですね。

というわけで「プロパティ名+"Changed"」という名前のイベントを定義する際には、以下のように宣言すればOKです。以上、業務で30分ほどハマった小ネタでした。

public static event EventHandler<PropertyChangedEventArgs>? CountChanged;
// 要は第1、第2引数がobject、PropertyChangedEventArgsなら大丈夫なので、これもOK
public static event Action<object?, PropertyChangedEventArgs>? CountChanged;

Discussion