ObservePropertyでArgumentException!?
MVVMで以下のようなコードをModelとViewModelに書いていました。なお、MVVMフレームワークにPrismを使用しています。Modelくんは可視状態をbool
型で持ち、ViewModelくんはそれをIObservable<bool>
に変換してからToReadOnlyReactiveProperty
でバインドするプロパティに変えます。
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; }
}
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#の構文的には合っているのに実行時にエラーになるということは、ObserveProperty
がArgumentException
をスローしているに違いありません(というかArgumentException
はぬるり
みたいなのと違って開発者がthrow
しないと来ないし)。
というわけでObserveProperty
の内部実装を見てみましょう。この神メソッドはGod of GodsプロジェクトであるReactivePropertyの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.csのIsNestedPropertyPathメソッドです。
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
メソッドの引数に渡されたラムダ式の構造をチェックしているというわけですね。詳細はわからなくても、引数の本体がMemberExpression
かUnaryExpression
であるラムダ式でないとダメなようです。Memberということはおそらくメンバ名をそのまま指定しないといけないのでしょう。Unaryはなんなんだろう?このあたりの想像はSouceGeneratorを作るためにRoslynコンパイラを触って構文解析した経験(宣伝)が生きます。
ここで、エラーになる行のコードをもう一度見てみます。
// 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>
にしたいメンバをそのまま指定して、その値に対して絞り込みなり編集なりしたければReactiveExtensions
でLinq
チックに実装をしましょう。ObserveProperty
メソッドに謁見できる存在はMemberExpression
かUnaryExpression
という選ばれし存在のみだからです。
最近のC#やNugetで取得できるライブラリはオープンソースばかりで、Githubにコードが公開されているのでいざとなったら直接中身を見て原因究明できるのがいいですね~!