Help us understand the problem. What is going on with this article?

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

More than 5 years have passed since last update.

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; }
    }
}
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away