PG日誌

受託系 PG が C# の事を書いています

WPF の PropertyChanged で使用するプロパティ名の文字列を動的に取得する

WPF で MVVM しようと思って ViewModel で INotifyPropertyChanged を実装した後に、プロパティ変更通知をベタ書きすると、以下のようになると思います。

this.PropertyChanged(this, new PropertyChangedEventArgs("Name"));

ただこの、"Name" の部分。文字列なんですよね。プロパティ名変更した時に一緒に変わらないんですよね。
たまーに、あれ?画面表示が変わらないな…なんてデグレが起きます。VisualStudio も名前変えた時に一緒に変更してくれないので、もう文字列を直接描くこと自体が問題だと思います。

あと、こインターフェースを親クラスに実装してそれを継承するなんてサンプルを見ますがリスコフの置換原則をわざわざ無視するほどの事でも無い気がします。

なので以下の対策を行おうかと思います。

.net4.5 より前での解決策

で、微妙に悩んでいたところ、こんな記事を見つけました。
未だに .nnet3.5 とか使ってる現場向けです。(というか 経験から言わせてもらうと WFP 使ってて .net が 3.5 で開発環境が VS2008 だと地獄です…)

プロパティからプロパティの名前(文字列)を取得する。

早速、こんなユーティリティを作ってみました。

/// <summary>
/// ViewModel 使用時の汎用操作を共通化します。
/// </summary>
public static class ViewModelUtil {

    /// <summary>
    /// ラムダ式の左辺に指定したプロパティの名称を取得します。
    /// 
    /// [利用例]
    ///   string str = "Test";
    ///   Console.WriteLine("{0} : {1}", GetName(() => str.Length), str.Length);
    ///   
    /// [出力]
    ///   >> Prints Length : 4
    ///   
    /// </summary>
    /// <typeparam name="T">名前を取得する対象プロパティの型</typeparam>
    /// <param name="e">取得したいプロパティを記述したラムダ式</param>
    /// <returns>プロパティ名</returns>
    public static string GetPropertyName<T>(Expression<Func<T>> e) 
    {
        return ((MemberExpression)e.Body).Member.Name;
    }

    /// <summary>
    /// プロパティ変更通知を行います。
    /// 
    /// [利用例]
    ///   // INotifyPropertyChanged のメンバー
    ///   public event PropertyChangedEventHandler PropertyChanged;
    ///   
    ///   private string _Name;
    ///   public string Name 
    ///   {
    ///       get { return this._Name; }
    ///       set 
    ///       {
    ///           this._Name = value;
    ///           ViewModelUtil.Notify(this, this.PropertyChanged, () => this.Name);
    ///       }
    ///   }
    /// 
    /// </summary>
    /// <typeparam name="T">名前を取得する対象プロパティの型</typeparam>
    /// <param name="sender">
    ///     <typeparamref name="PropertyChangedEventArgs"/> の第1引数と等価です。</param>
    /// <param name="p">使用する 
    ////    <typeparamref name="PropertyChangedEventHandler"/> を指定します。</param>
    /// <param name="e">取得したいプロパティを記述したラムダ式</param>
    public static void Notify<T>(object sender, 
        PropertyChangedEventHandler p, Expression<Func<T>> e) 
    {
        if (p == null) 
        {
            return;
        }
        p(sender, new PropertyChangedEventArgs(ViewModelUtil.GetPropertyName(e)));
    }
}

呼び出し側からは、コメントにもありますが以下のように記述します。

ViewModelUtil.Notify(this, this.PropertyChanged, () => this.Name);

これで文字列埋め込み不要、スーパークラス不要になりました。
が、、、ここでラムダ式が出現する理由がよく分らないので微妙に微妙ですね。。。

.net4.5 以降の解決策

c #5.0の新機能

こんな機能があるので、拡張メソッドにちょちょっと細工します。

public static class PropertyChangedEventHandlerExtension {

    /// <summary>
    /// <typeparamref name="PropertyChangedEventHandler"/> の呼び出しを簡略化します。
    /// </summary>
    /// <param name="eh">拡張対象メソッド名</param>
    /// <param name="sender">
    ///     <typeparamref name="PropertyChangedEventHandler"/> の sender と等価です。</param>
    /// <param name="name">
    ///     <typeparamref name="PropertyChangedEventArgs"/> の propertyName と等価です。</param>
    public static void Notice(this PropertyChangedEventHandler eh, 
        object sender, [CallerMemberName] string name = "") 
    {
        if (eh == null)
        {
            return;
        }
        eh(sender, new PropertyChangedEventArgs(name));
    }
}

こうすると呼び出し側では、

this.PropertyChanged.Notice(this);

と、もう this 指定するだけで呼び出せる謎メソッド化します。
現場で .net4.5 以降使えるなら断然こっちですね。

sender とかいらねーよって人は拡張メソッドのデフォルト引数に sender = null とかにして引数無しで呼び出すこともできそうですね。後で痛い目に合いそうですが。