プログラミング C# - 翔ソフトウェア (Sho's)

C#/.NET/ソフトウェア開発など

C# による Observer パターンの実装 その5 - Caller Info を使ってプロパティの指定をよりシンプルに

Observer

前回「C# による Observer パターンの実装 その4 - Expression を使ってプロパティの指定をタイプセーフに」と云う記事で、Observer パターンの C# による実装の第四回として、Expression を用いることで、前々回文字列でプロパティを指定していた部分をタイプセーフにしてみた。

今回は、それに対する補足だ。
前回の Expression による遣り方とは別の方法で更にアプリケーション部をシンプルにしてみたい。
C# 5 の新機能の Caller Info を使った方法だ。

■ 前回のソースコード

先ず前回のフレームワーク部の Observable のソースコードを再掲する。

フレームワーク部 - Observable (前回)
// C# による Oberver パターンの実装 その4
using System;
using System.Collections.Generic;
using System.Linq.Expressions;

abstract class Observable // 更新を監視される側
{
    public event Action<string> Update;

    protected void RaiseUpdate<PropertyType>(Expression<Func<PropertyType>> propertyExpression)
    {
        RaiseUpdate(ObjectExtensions.GetMemberName(propertyExpression));
    }

    void RaiseUpdate(string propertyName)
    {
        if (Update != null)
            Update(propertyName);
    }
}

Observable の RaiseUpdate において、Expression で受けることでプロパティ名を動的に取得している。

この部分を C#5.0 の CallerInfo を使う形で書き換えてみよう。

Caller Info 属性の一つである [CallerMemberName] を付けておくと、呼び出し元のメンバー名 (この場合はプロパティ名) がコンパイラーによって渡されてくる。
これを利用してみよう。

フレームワーク部 - Observable (今回)
// C# による Oberver パターンの実装 その5
using System;
using System.Runtime.CompilerServices; // ☆ [CallerMemberName] の為に必要

abstract class Observable // 更新を監視される側
{
    public event Action<string> Update;

    protected void RaiseUpdate([CallerMemberName] string propertyName = "") // ☆ Caller Info 属性を利用
    {
        if (Update != null)
            Update(propertyName);
    }
}

すると、Observable の派生クラスで、プロパティ名を渡さなくて良くなる。

前回の Employee はこうだった。

・アプリケーション部 - Employee と EmployeeView (前回)
// C# による Oberver パターンの実装 その4
using System;

// アプリケーション部

// Model
class Employee : Observable
{
    int number = 0;
    string name = string.Empty;

    public int Number
    {
        get { return number; }
        set {
            if (value != number) {
                number = value;
                RaiseUpdate*1
            updateAction(dataSource.Eval(propertyName));
    }
}
// C# による Oberver パターンの実装 その5
using System;

// アプリケーション部

// Model
class Employee : Observable
{
    int number = 0;
    string name = string.Empty;

    public int Number
    {
        get { return number; }
        set {
            if (value != number) {
                number = value;
                RaiseUpdate(); // ☆ RaiseUpdate の中で文字列でプロパティを渡す必要がなくなった
            }
        }
    }

    public string Name
    {
        get { return name; }
        set {
            if (value != name) {
                name = value;
                RaiseUpdate(); // ☆ RaiseUpdate の中で文字列でプロパティを渡す必要がなくなった
            }
        }
    }
}

class TextControl // テキスト表示用のUI部品 (ダミー)
{
    public string Text
    {
        set { Console.WriteLine("TextControl is updated: {0}", value); }
    }
}

class EmployeeView : Observer<Employee> // Employee 用の View
{
    TextControl numberTextControl = new TextControl(); // Number 表示用
    TextControl nameTextControl = new TextControl(); // Name 表示用

    public EmployeeView()
    {
        AddUpdateAction(employee => employee.Number, number => numberTextControl.Text = *2;
        AddUpdateAction(employee => employee.Name, name => nameTextControl.Text = (string)name);
    }
}

class Program
{
    static void Main()
    {
        var employee = new Employee(); // Model, Observable
        var employeeView = new EmployeeView(); // View, Observer

        employeeView.DataSource = employee; // データバインド
        employee.Number = 100; // Number を変更。employeeView に反映されたかな?
        employee.Name = "福井太郎"; // Name を変更。employeeView に反映されたかな?
    }
}
・実行結果

勿論、実行結果に変わりはない。

TextControl is updated: 100
TextControl is updated: 福井太郎

■ 今回のまとめと次回予告

今回は、前回のものに対して補足を行った。

Caller Info を使うことで、アプリケーション部でプロパティを指定する箇所が更にシンプルになった。

次回は、DynamicObject を使った全然別のアプローチを試してみたい。

お楽しみに。

*1:) => Number); // ☆ RaiseUpdate がタイプセーフに } } } public string Name { get { return name; } set { if (value != name) { name = value; RaiseUpdate(() => Name); // ☆ RaiseUpdate がタイプセーフに } } } }

これがこうなる。

・アプリケーション部 - Employee と EmployeeView (今回)
// C# による Oberver パターンの実装 その5
using System;

// アプリケーション部

// Model
class Employee : Observable
{
    int number = 0;
    string name = string.Empty;

    public int Number
    {
        get { return number; }
        set {
            if (value != number) {
                number = value;
                RaiseUpdate(); // ☆ RaiseUpdate の中で文字列でプロパティを渡す必要がなくなった
            }
        }
    }

    public string Name
    {
        get { return name; }
        set {
            if (value != name) {
                name = value;
                RaiseUpdate(); // ☆ RaiseUpdate の中で文字列でプロパティを渡す必要がなくなった
            }
        }
    }
}

前回のような動的にプロパティ名を取得する方法と異なり、コンパイラーが渡してくれるので、オーバーヘッドも小さくなる筈だ。

・全体のソースコード (今回)

全体のソースコードはこうなった。

// C# による Oberver パターンの実装 その5
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Runtime.CompilerServices; // ☆ [CallerMemberName] の為に必要

// フレームワーク部

public static class ObjectExtensions
{
    // オブジェクトの指定された名前のプロパティの値を取得
    public static object Eval(this object item, string propertyName)
    {
        var propertyInfo = item.GetType().GetProperty(propertyName);
        return propertyInfo == null ? null : propertyInfo.GetValue(item, null);
    }

    // Expression からメンバー名を取得
    public static string GetMemberName<ObjectType, MemberType>(this ObjectType @this, Expression<Func<ObjectType, MemberType>> expression)
    {
        return ((MemberExpression)expression.Body).Member.Name;
    }
}

abstract class Observable // 更新を監視される側
{
    public event Action<string> Update;

    protected void RaiseUpdate([CallerMemberName] string propertyName = "") // ☆ Caller Info 属性を利用
    {
        if (Update != null)
            Update(propertyName);
    }
}

abstract class Observer<ObservableType> where ObservableType : Observable // 更新を監視する側
{
    Dictionary<string, Action<object>> updateExpressions = new Dictionary<string, Action<object>>();
    ObservableType dataSource = null;

    public ObservableType DataSource
    {
        set {
            dataSource = value;
            value.Update += Update;
        }
    }
    
    protected void AddUpdateAction<PropertyType>(Expression<Func<ObservableType, PropertyType>> propertyExpression, Action<object> updateAction)
    {
        AddUpdateAction(dataSource.GetMemberName(propertyExpression), updateAction);
    }

    void AddUpdateAction(string propertyName, Action<object> updateAction)
    {
        updateExpressions[propertyName] = updateAction;
    }

    void Update(string propertyName)
    {
        Action<object> updateAction;
        if (updateExpressions.TryGetValue(propertyName, out updateAction

*2:int)number).ToString(