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

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

C# による Observer パターンの実装 その4 - Expression を使ってプロパティの指定をタイプセーフに

Observer

前回「C# による Observer パターンの実装 その3 - 複数のプロパティの更新イベントをフレームワーク側で振り分け」と云う記事で、Observer パターンの C# による実装の第三回として、フレームワーク部で、プロパティ毎の更新処理が呼ばれるようにした。

結果として、アプリケーション部の Employee でプロパティ毎に更新イベントを用意する必要がなくなった。

しかし、アプリケーション部の一部に文字列でプロパティを指定する部分が出来てしまった。

そこで、今回は、「C# による Observer パターンの実装 その4 - Expression を使ってプロパティの指定をタイプセーフに」として、文字列でプロパティを指定する部分を解消してみよう。

C# での Observer パターンの実装 4

以前、「Expression を使ってラムダ式のメンバー名を取得する」と云う記事で、Expression を使ってラムダ式からプロパティ名を取得したことがある。

これを用いることが出来る。

フレームワーク部 - ObjectExtensions への追加

前回の ObjectExtensions に、件の記事から、Expression からメンバー名を取得する部分を持ってこよう。

using System;
using System.Linq.Expressions;

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 からメンバー名を取得 (「Expression を使ってラムダ式のメンバー名を取得する」で作成)
    public static string GetMemberName<ObjectType, MemberType>(this ObjectType @this, Expression<Func<ObjectType, MemberType>> expression)
    {
        return *1
            updateAction(dataSource.Eval(propertyName));
    }
}

これの、文字列でプロパティ名を受けている箇所に、Expression で受けるメソッドを追加する。

こうだ。

// 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);
    }
}

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))
            updateAction(dataSource.Eval(propertyName));
    }
}
・アプリケーション部 - Employee と EmployeeView の変更

すると、前回のアプリケーション部の Employee と EmployeeView のソースコード:

// C# による Oberver パターンの実装 その3 (前回)
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("Number"); // ☆ 文字列でプロパティを指定
            }
        }
    }

    public string Name
    {
        get { return name; }
        set {
            if (value != name) {
                name = value;
                RaiseUpdate("Name"); // ☆ 文字列でプロパティを指定
            }
        }
    }
}

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

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

    public EmployeeView()
    {
        // ☆ AddUpdateAction の第一引数で文字列でプロパティを指定
        AddUpdateAction("Number", number => numberTextControl.Text = *2;
        AddUpdateAction("Name", name => nameTextControl.Text = (string)name);
    }
}

これが、こうなる。

// 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*3;
        AddUpdateAction(employee => employee.Name, name => nameTextControl.Text = (string)name);
    }
}

文字列でプロパティを指定している箇所が消えた。

・Main を含んだクラス Program

Main を含んだクラス Program は、変更なし。

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: 福井太郎

矢張り、Number と Name それぞれの更新で、それぞれの TextControl が更新されている。

・クラス図

さてクラス図はこうなった。
前回と余り変わらない。

「C# での Observer パターンの実装 4」のクラス図
C# での Observer パターンの実装 4」のクラス図
「C# での Observer パターンの実装 4」のクラス図 (引数と戻り値の型を書き加えたもの)
C# での Observer パターンの実装 4」のクラス図 (引数と戻り値の型を書き加えたもの)

例によって、青い部分がフレームワーク部、赤い部分がアプリケーション部だ。

■ 今回のまとめ

今回は、前回のものに対して、プロパティを文字列で指定している部分を改良した。

そうすることで、アプリケーション部でプロパティを指定する箇所がタイプセーフになった。

アプリケーション部が随分すっきりと書けるようになったのが判る。

■ 次回予告

さて次回だが、今回 Expression を使って行ったのと同様のことを別の方法で実現してみたい。

お楽しみに。

*1:MemberExpression)expression.Body).Member.Name; } // Expression からメンバー名を取得 (「Expression を使ってラムダ式のメンバー名を取得する」で作成) public static string GetMemberName<MemberType>(Expression<Func<MemberType>> expression) { return ((MemberExpression)expression.Body).Member.Name; } }

フレームワーク部 - Observable と Observer の変更

そして、前回のフレームワーク部の Observable と Observer のソースコード:

// C# による Oberver パターンの実装 その3 (前回)
using System;
using System.Collections.Generic;

// フレームワーク部

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

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

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

    public Observable DataSource
    {
        set {
            dataSource = value;
            value.Update += Update;
        }
    }

    protected 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(

*3:) => Number); // ☆ RaiseUpdate がタイプセーフに } } } public string Name { get { return name; } set { if (value != name) { name = value; RaiseUpdate(() => Name); // ☆ 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 の第一引数がタイプセーフに AddUpdateAction(employee => employee.Number, number => numberTextControl.Text = ((int)number).ToString(