LoginSignup
8
9

More than 5 years have passed since last update.

x => x.Hoge.Fuga.Piyo と指定しても正しく PropertyChanged イベントを受け取る

Posted at

Livet の PropertyChangedEventListener を見ていてふと思いついたので。
PropertyChangedEventListener の場合は、 以下のようにして特定のプロパティに限定した PropertyChanged イベントハンドラを追加します。

var listener = new PropertyChangedEventListener(model);
listener.RegisterHandler(() => model.Value, (s, e) =>
{
    // Value プロパティが変更した時にだけ実行する処理
});

もし、 Value プロパティも INotifyPropertyChanged を実装したオブジェクトで、入れ子になったプロパティの変更も監視したい場合、イベント購読の処理が若干面倒になりますね。

一方、 XAML の Binding 機構では以下のように指定してもしっかり追従してくれます。

<TextBlock Text="{Binding Hoge.Fuga.Piyo.Value}" />

そこで、上記の Binding のように階層化したプロパティにも追従するイベントリスナーを書いてみました。
リフレクションを多用しているのでかなり遅いと思います。

下記のクラスをを使うと以下のようにイベントハンドラを追加できます。

var listener = new PropertyListener<Model>(model);
listener.RegisterHandler(x => x.Hoge.Fuga.Piyo.Value, () =>
{
    // 処理
});

// 以下のどの処理でもイベントハンドラが実行される
model.Hoge = hogeHoge;
model.Hoge.Fuga= fugaFuga;
model.Hoge.Fuga.Piyo= piyoPiyo;
model.Hoge.Fuga.Piyo.Value = newValue;
PropertyListener.cs
public class PropertyListener<TObj> : IDisposable
    where TObj : INotifyPropertyChanged
{
    public PropertyListener(TObj obj)
    {
        _eventSource = obj;
    }

    private TObj _eventSource;
    private Action _disoposeAction = () => { };

    public void RegisterHandler<TProp>(Expression<Func<TObj, TProp>> propExp, Action handler)
    {
        var pt = CreatePropertyTree(propExp.Body);
        RegisterChildHandler<TObj>(this, pt, handler);
    }

    // 式木とは逆順のプロパティ木構造を作成する。
    private PropertyTree CreatePropertyTree(Expression exp, PropertyTree child = null)
    {
        var mExp = exp as MemberExpression;
        if (mExp == null)
        {
            return null;
        }
        var pi = mExp.Member as PropertyInfo;
        if (pi == null)
        {
            throw new ArgumentException("式木からプロパティを取得できません。");
        }
        var tree = new PropertyTree(pi, child);
        var parent = CreatePropertyTree(mExp.Expression, tree);
        if (parent != null)
        {
            return parent;
        }
        return tree;
    }

    // 再帰的に PropertyListener を登録していく。
    private void RegisterChildHandler<TChild>(PropertyListener<TChild> listener, PropertyTree tree, Action handler)
        where TChild : INotifyPropertyChanged
    {
        var propName = tree.PropertyInfo.Name;

        Action createChildListener = () => { };     // リスナー作成用のデリゲート
        Action disposeChildListener = () => { };    // リスナー解放用のデリゲート
        if (tree != null)
        {
            var methodName = MethodBase.GetCurrentMethod().Name;
            createChildListener = () =>
            {
                // プロパティを取得するデリゲートを作成&実行
                var pi = tree.PropertyInfo;
                var getterMi = pi.GetGetMethod();
                var getterType = typeof(Func<,>).MakeGenericType(pi.ReflectedType, pi.PropertyType);
                var childValue = Delegate
                    .CreateDelegate(getterType, getterMi)
                    .DynamicInvoke(listener._eventSource);

                // プロパティ(子供)に対して PropertyListener を作成。
                var childType = childValue.GetType();
                object childListener = null;
                if (typeof(INotifyPropertyChanged).IsAssignableFrom(childType))
                {
                    childListener = typeof(PropertyListener<>)
                        .MakeGenericType(childType)
                        .GetConstructor(new Type[] { childType })
                        .Invoke(new object[] { childValue });

                    // 解放用の処理を作成
                    disposeChildListener = () =>
                    {
                        var d = childListener as IDisposable;
                        if (d != null)
                        {
                            d.Dispose();
                        };
                    };

                    // 型パラメータを変えて再帰呼び出し。
                    listener
                        .GetType()
                        .GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)
                        .MakeGenericMethod(childType)
                        .Invoke(listener, new object[] { childListener, tree.Child, handler });
                }
            };
        }
        // 上で作成したデリゲートを実行
        createChildListener();

        PropertyChangedEventHandler pcHandler = (_, e) =>
        {
            if (e.PropertyName == propName)
            {
                handler();              // 引数で渡された処理を実行
                disposeChildListener(); // 古いリスナーを解放
                createChildListener();  // 新しいプロパティに対してリスナーを作成
            }
        };

        // イベントハンドラを登録&解放用の処理を追加
        listener._eventSource.PropertyChanged += pcHandler;
        _disoposeAction += () =>
        {
            listener._eventSource.PropertyChanged -= pcHandler;
            disposeChildListener();
        };
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            _disoposeAction();
            _eventSource = default(TObj);
        }
    }

    private class PropertyTree
    {
        public PropertyTree(PropertyInfo pi, PropertyTree child)
        {
            PropertyInfo = pi;
            Child = child;
        }
        public PropertyInfo PropertyInfo { get; private set; }
        public PropertyTree Child { get; set; }
    }
}
8
9
0

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
8
9