LoginSignup
4
2

More than 1 year has passed since last update.

INotifyPropertyChanged.ObservePropertyの引数に指定できるのは選ばれし者だけという話

Last updated at Posted at 2021-09-15

ObservePropertyでArgumentException!?

MVVMで以下のようなコードをModelとViewModelに書いていました。なお、MVVMフレームワークにPrismを使用しています。Modelくんは可視状態をbool型で持ち、ViewModelくんはそれをIObservable<bool>に変換してからToReadOnlyReactivePropertyでバインドするプロパティに変えます。

Model.cs
public class MyModel : BindableBase
{
    // PG内部で持ち回る可視状態(PrismのBindableBaseでINotifyPropertyChangedを実装)
    public bool IsVisible { get => _isVisible; set => SetProperty(ref _isVisible, value); }
    private bool _isVisible = false;

    public void なんかモデル的なメソッド() { IsVisible = !IsVisible; }
}

ViewModel.cs
public class ViewModel
{
    private readonly MyModel _model;
    // 画面にバインドする可視状態
    public ReadOnlyReactiveProperty<Visibility> VisibilityKind { get; }
    public ViewModel(MyModel model)
    {
        _model = model;
        // BooleanConverterだとCollapsedになってヤなので自分でHiddenに設定する
        VisiblityKind = _model.ObserveProperty(m => m.IsVisible ? 
            Visibility.Visible : Visibility.Hidden
        ).ToReadOnlyReactiveProperty();
    }
}

VMのソースはコンパイルできますが、ObservePropertyの行を通るとArgumentExceptionが起きます。
アイエエエ!ArgumentException!?ArgumentExceptionナンデ!?

例外をスローしている箇所

C#の構文的には合っているのに実行時にエラーになるということは、ObservePropertyArgumentExceptionをスローしているに違いありません(というかArgumentExceptionぬるりみたいなのと違って開発者がthrowしないと来ないし)。
というわけでObservePropertyの内部実装を見てみましょう。この神メソッドはGod of GodsプロジェクトであるReactivePropertyINotifyPropertyChangedExtensions.csに実装されていらっしゃいます。以下、コードを引用します。インデントだけ調整させてもらいました。

INotifyPropertyChangedExtensions.cs
    public static IObservable<TProperty> ObserveProperty<TSubject, TProperty>(
        this TSubject subject, Expression<Func<TSubject, TProperty>> propertySelector,
        bool isPushCurrentValueAtFirst = true)
        where TSubject : INotifyPropertyChanged
    {
        return ExpressionTreeUtils.IsNestedPropertyPath(propertySelector) ?
            ObserveNestedProperty(subject, propertySelector, isPushCurrentValueAtFirst) :
            ObserveSimpleProperty(subject, propertySelector, isPushCurrentValueAtFirst);
    }

ObserveProperty自体は例外をスローしていません。ということは内部で呼んでいるメソッドが怪しいです。上から順に見ていきましょう。まずはExpressionTreeUtils.csIsNestedPropertyPathメソッドです。

ExpressionTreeUtils.cs
    public static bool IsNestedPropertyPath<TSubject, TProperty>(Expression<Func<TSubject, TProperty>> propertySelector)
    {
        if (propertySelector.Body is MemberExpression member)
        {
            return !(member.Expression is ParameterExpression);
        };

        if (propertySelector.Body is UnaryExpression unary)
        {
            if (unary.Operand is MemberExpression unaryMember)
            {
                return !(unaryMember.Expression is ParameterExpression);
            }
        }

        throw new ArgumentException();
    }

ArgumentException投げてるじゃん!というわけで元のコードが特定できたので、コードを読んで原因を調べていきます。

原因

このコードでは引数となるExpression<Func<TSubject, TProperty>>型のpropertySelectorを調べて、条件に該当しなければ例外をスローしているようです。そもそもExpression<Func<T>>ってなんやねん?というと、ラムダ式(の式木)を示す型のようです。つまり、ObservePropertyメソッドの引数に渡されたラムダ式の構造をチェックしているというわけですね。詳細はわからなくても、引数の本体がMemberExpressionUnaryExpressionであるラムダ式でないとダメなようです。Memberということはおそらくメンバ名をそのまま指定しないといけないのでしょう。Unaryはなんなんだろう?このあたりの想像はSouceGeneratorを作るためにRoslynコンパイラを触って構文解析した経験(宣伝)が生きます。
ここで、エラーになる行のコードをもう一度見てみます。

ViewModel.cs
        // BooleanConverterだとCollapsedになってヤなので自分でHiddenに設定する
        VisiblityKind = _model.ObserveProperty(m => m.IsVisible ? 
            Visibility.Visible : Visibility.Hidden
        ).ToReadOnlyReactiveProperty();

ぜんぜんMemberExpressionっぽくないですね。普通に式を書いてしまっています。
そもそも、IObservable<T>の内容を変えてReactivePropertyにしたかったらReactiveExtensions

_model.ObserveProperty(m => m.IsVisible). //IObservable<bool>を
       Select(b => b ? Visibility.Visible : Visibility.Hidden). //IObservable<Visibility>にして
       ToReadOnlyReactiveProperty(); // ReactivePropertyにする

こんなふうに書かないといけないのでした。これでArgumentExceptionは起きません。めでたしめでたし

まとめ

絶対神ObservePropertyメソッドを使うにはIObservable<T>にしたいメンバをそのまま指定して、その値に対して絞り込みなり編集なりしたければReactiveExtensionsLinqチックに実装をしましょう。ObservePropertyメソッドに謁見できる存在はMemberExpressionUnaryExpressionという選ばれし存在のみだからです。
最近のC#やNugetで取得できるライブラリはオープンソースばかりで、Githubにコードが公開されているのでいざとなったら直接中身を見て原因究明できるのがいいですね~!

4
2
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2