読者です 読者をやめる 読者になる 読者になる
ようこそ。睡眠不足なプログラマのチラ裏です。

MVVMパターンのViewModelで、INotifyPropertyChangedをいちいち実装すんのかったるい。かといって、AbstractなViewModelBaseは継承したくないでござるよ。

プログラミング C#3.0 C#4.0 MVVMパターン

INotifyPropertyChanged インターフェースを実装した抽象クラス ViewModelBase は、残念な俺俺設計

MVVMパターンでViewModelを実装する場合、INotifyPropertyChanged インターフェースを実装するのが面倒という理由もあって、
INotifyPropertyChanged インターフェースを実装した抽象クラス「 ViewModelBase 」を用意したりすることが、
世の中的に割と定番となっているような雰囲気がありますが、これはお世辞にもあまりよい設計とは言えないと思う。
確かに、ViewModel は INotifyPropertyChanged インターフェイスを実装したものかもしれないが、
これは実装を簡略化することだけを目的とした俺俺設計にすぎないからです。


抽象クラスはインターフェイスよりも抽象的な表現力が低いということ

MVVM パターンにおいて、ViewModel は「 INotifyPropertyChanged を実装した View とバインドできる何か」でさえあればよいのに、
ViewModel が ViewModelBase を必ず継承する必要があるというのは、誰が見ても具合が悪いです。
そもそも抽象クラスはインターフェイスよりも抽象度が低いものなので、抽象クラスによる表現は
インターフェイスとして表現するよりも抽象的な表現力が低くなってしまいます。
ならば、どのように設計・実装したらよいのでしょう。あまり深く考える必要はありません。
インターフェイスを、そのままインターフェイスで表現すれば抽象的な表現力は落ちずに済みます。
インターフェイスで INotifyPropertyChanged の簡易実装を表現することを考えます。


IImpleNotifyPropertyChanged インターフェイスとその他もろもろ


で、こうなった。
ネーミングがいまいちですが、いいの思いついたら変えますw


IImpleNotifyPropertyChanged.cs

using System;
using System.Linq;
using System.Linq.Expressions;
using System.ComponentModel;

namespace ClassLibrary1
{
    /// <summary>
    /// IImpleNotifyPropertyChangedインターフェイス
    /// 
    /// INotifyPropertyChangedの実装をある程度抽象化し、簡易な実装を提供します。
    /// </summary>
    public interface IImpleNotifyPropertyChanged : INotifyPropertyChanged
    {
        /// <summary>
        /// PropertyChangedの再定義
        /// (INotifyPropertyChangedのPropertyChangedを隠蔽する)
        /// </summary>
        new event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// PropertyChangedEventHandlerを取得します。
        /// </summary>
        PropertyChangedEventHandler PropertyChangedHandler { get; }
    }

    /// <summary>
    /// IImpleNotifyPropertyChangedインターフェイスについて拡張メソッド提供
    /// </summary>
    public static class IImpleNotifyPropertyChangedExtentions
    {
        /// <summary>
        /// プロパティの変更を通知します。
        /// </summary>
        /// <param name="self">対象</param>
        /// <param name="targets">プロパティ変更通知対象の名称群</param>
        private static void OnPropertyChanged<TObject>(this TObject self, params string[] propertyNames)
            where TObject : IImpleNotifyPropertyChanged
        {
            if (self == null) return;
            if (self.PropertyChangedHandler == null) return;

            foreach (var propertyName in propertyNames)
                self.PropertyChangedHandler(self, new PropertyChangedEventArgs(propertyName));
        }

        /// <summary>
        /// プロパティの変更を通知します。
        /// </summary>
        /// <typeparam name="TObject"></typeparam>
        /// <typeparam name="TExpression"></typeparam>
        /// <param name="self">対象</param>
        /// <param name="expressions">プロパティ変更通知対象の名称を取得するExpression群</param>
        public static void OnPropertyChanged<TObject, TExpression>(this TObject self, params Expression<Func<TObject, TExpression>>[] expressions)
            where TObject : IImpleNotifyPropertyChanged
        {
            var propertyNames = from e in expressions
                                select ((MemberExpression)e.Body).Member.Name;
            self.OnPropertyChanged(propertyNames.ToArray());
        }
    }

    /// <summary>
    /// INotifyPropertyChangedインターフェイスについて拡張メソッドを提供
    /// </summary>
    public static class INotifyPropertyChangedExtentions
    {
        /// <summary>
        /// プロパティ変更イベントの関連付け補助
        /// </summary>
        /// <typeparam name="TObject"></typeparam>
        /// <typeparam name="TExpression"></typeparam>
        /// <param name="self"></param>
        /// <param name="expressions"></param>
        /// <param name="handler"></param>
        public static void AddPropertyChanged<TObject, TExpression>(this TObject self, Expression<Func<TObject, TExpression>> expressions, Action<TObject> handler)
            where TObject : INotifyPropertyChanged
        {
            if (self == null) return;
            var propName = ((MemberExpression)expressions.Body).Member.Name;
            self.PropertyChanged += (sender, e) =>
            {
                if (e.PropertyName == propName)
                    handler(self);
            };
        }
    }
}

実装イメージはこう。楽々ヒャッハー!


ViewModelSample.cs

using System.Diagnostics;
using System.ComponentModel;
using ClassLibrary1;

namespace WpfApplication1
{
    public class ViewModelSample : IImpleNotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        PropertyChangedEventHandler IImpleNotifyPropertyChanged.PropertyChangedHandler
        {
            get { return PropertyChanged; }
        }

        public ViewModelSample()
        {
            this.AddPropertyChanged(x => x.Hoge, HogeChanged);
        }

        private string _hoge;
        public string Hoge
        {
            get { return _hoge; }
            set
            {
                if (_hoge == value) return;
                _hoge = value;
                this.OnPropertyChanged(o => o.Hoge);
            }
        }

        private static void HogeChanged(ViewModelSample p)
        {
            Debug.Print(p.Hoge + "が変更されたよ!");
        }
    }
}


なお、AddPropertyChanged メソッドについては、わんくま同盟のかずきさん記事を完全にパクらせていただきました。
思いつくようで思いついてなかった素敵アイディア。ありがたありがたや〜。


PropertyChangedイベントの処理方法 - かずきのBlog@Hatena
http://d.hatena.ne.jp/okazuki/20091210/1260417214