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

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

C# による Observer パターンの実装 その6 - DynamicObject を使ってオブザーバーを作る

C# Advent Calender 2012 の 25日目のエントリー。
Observer

本ブログでは、これ迄五回に渡り、C# による Observer パターンの実装をご紹介してきた。

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

今回は、同じく第三回からの改良を行ってみたい。
第四回第五回の遣り方とは別のアプローチでアプリケーション部をシンプルにしてみたい。

■ DynamicObject の応用

以前、「DynamicObject を使ってみよう」や「DynamicObject を使ってみよう その 2」と云う記事で DynamicObject をご紹介した。

これらの記事で、DynamicObject を用いることで、プロパティの設定時の処理を定義出来ることを示した。

今回は、この仕組みを用いて、フレームワーク部の Observable で動的にプロパティの更新を捕まえ、更新イベントを発行してみよう。

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

・クラス図

全体像をざっと把握していただくために、先ずクラス図を示そう。

「C# による Observer パターンの実装 その6」のクラス図
C# による Observer パターンの実装 その6」のクラス図

引数と戻り値の型を書き加えたクラス図だとこうなる。

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

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

フレームワーク部の実装

では、フレームワーク部から実装していこう。

フレームワーク部の実装 - ObjectExtensions

第三回の ObjectExtensions を少し書き換えて、今回の DynamicObject を用いた例に対応できるようにする。

using System.Dynamic;

// フレームワーク部

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

    // オブジェクトの指定された名前のプロパティの値を設定
    public static void SetPropertyValue(this object item, string propertyName, object value)
    {
        var propertyInfo = item.GetType().GetProperty(propertyName);
        if (propertyInfo != null)
            propertyInfo.SetValue(item, value, null);
    }

    // DynamicObject の指定された名前のプロパティの値を取得
    public static object GetPropertyValue(this DynamicObject item, string propertyName)
    {
        object result;
        return item.TryGetMember(new MyGetMemberBinder(propertyName), out result) ? result : null;
    }
    
    // GetPropertyValue 用
    class MyGetMemberBinder : GetMemberBinder
    {
        public MyGetMemberBinder(string name) : base(name, false)
        { }

        public override DynamicMetaObject FallbackGetMember(DynamicMetaObject target, DynamicMetaObject errorSuggestion)
        { return null; }
    }
}

object に指定された名前のプロパティの値を設定する拡張メソッドや、DynamicObject から指定された名前のプロパティの値を取得する拡張メソッドを追加した。

フレームワーク部の実装 - DynamicContainer

DynamicContainer は、「DynamicObject を使ってみよう」に出てきたのと同様、DynamicObject の派生クラスだ。

あの時と同じように、TrySetMember と TryGetMember をオーバーライドする。

あの時との違いは、target としてのオブジェクトを持ち、値を設定したり取得したりする際はそちらに行うことだ。

従って、Dictionary の中には値を保持する必要がない。

最初に target を受け取った際に、その target の全プロパティの型情報のみ格納しておく
(本当は、各プロパティが public な set と public な get の両方を持つかどうかの情報も保持した方が良いが、今回は省略)。

using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Reflection;

// フレームワーク部

// 任意のオブジェクトを格納してプロパティの値の変化を監視
class DynamicContainer : DynamicObject
{
    // 対象とするアイテム
    object target;
    // observableTarget の該当するプロパティ名毎に型を格納
    Dictionary<string, Type> types = new Dictionary<string, Type>();

    public DynamicContainer(object target)
    {
        this.target = target;
        SetProperties(target.GetType()); // 対象とする型の全プロパティを格納
    }

    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        Type type;
        // 該当するプロパティの型を values から取得
        if (types.TryGetValue(binder.Name, out type)) {
            var valueType = value.GetType();
            // もしプロパティに設定しようとしている値の型が、そのプロパティの型もしくはそのサブクラスだったら
            if (valueType.Equals(type) || valueType.IsSubclassOf(type)) {
                // target のプロパティに値を設定
                target.SetPropertyValue(binder.Name, value);
                return true;
            }
        }
        return false;
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        // target のプロパティから値を取得
        bool isSucceeded;
        result = target.Eval(binder.Name, out isSucceeded);
        return isSucceeded;
    }

    // 対象とする型の全プロパティを格納
    void SetProperties(Type type)
    {
        type.GetProperties().ToList().ForEach(SetProperty);
    }

    // 対象とするアイテムのプロパティを格納
    void SetProperty(PropertyInfo propertyInfo)
    {
        types[propertyInfo.Name] = propertyInfo.PropertyType;
    }
}
フレームワーク部の実装 - DynamicObservable

次に、DynamicContainer を継承して DynamicObservable を作る。

前回迄の Observable にあたるクラスだ。

Observable と異なり、抽象クラスではない。

これまでは、イベントを起こす為のメソッド RaiseUpdate をアプリケーション側のモデルで、変更されるプロパティ毎に呼ぶ必要があった。

この DynamicObservable では、TrySetMember をオーバーライドし、もし値が更新された場合は、自ら Update イベントを起こす。

これにより、アプリケーション部のモデルがシンプルになる筈だ。

using System;
using System.Dynamic;

// フレームワーク部

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

    public DynamicObservable(object target) : base(target)
    { }

    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        object oldValue = this.GetPropertyValue(binder.Name);
        if (base.TrySetMember(binder, value)) {
            if (!value.Equals(oldValue))
                RaiseUpdate(binder.Name);
            return true;
        }
        return false;
    }

    void RaiseUpdate(string propertyName)
    {
        if (Update != null)
            Update(propertyName);
    }
}
フレームワーク部の実装 - DynamicObserver

フレームワーク部の実装の最後は、DynamicObserver だ。

前回迄の Observer にあたるクラスだ。

このクラスは、DataSource が Observable から DynamicObservable に変化した以外は、変更はない。

using System;
using System.Collections.Generic;

// フレームワーク部

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

    public DynamicObservable 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))
            updateAction(dataSource.GetPropertyValue(propertyName));
    }
}
・アプリケーション部の実装

次に、アプリケーション部だ。
どんな風にシンプルになっただろう。

・アプリケーション部の実装 - Employee

Employee は、こうなる。

プロパティが設定される毎に Update イベントを起こさなくて良くなったため、随分シンプルだ。

特定のクラスからの派生が不要な POCO (Plain Old CLR Object) になった。

// アプリケーション部

// Model
class Employee
{
    public int Number { get; set; }
    public string Name { get; set; }
}
・アプリケーション部の実装 - EmployeeView

EmployeeView は、こうだ。

第三回から全く変化していない。

using System;

// アプリケーション部

class TextControl // テキスト表示用のUI部品 (ダミー)
{
    string text = string.Empty;

    public string Text
    {
        set { text = value; Console.WriteLine("TextControl is updated: {0}", value); }
        get { return text; }
    }
}

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

    public EmployeeView()
    {
        AddUpdateAction("Number", number => numberTextControl.Text = number.ToString());
        AddUpdateAction("Name", name => nameTextControl.Text = (string)name);
    }
}
・アプリケーション部の実装 - Main を含んだクラス Program

Main を含んだクラス Program では、少し変更がある。

EmployeeView のデータソースに DynamicObservable を用いるようにする。

using System;

// アプリケーション部

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

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

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

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

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

今回は、もう少し念入りにチェックしてみよう。

using System;

// アプリケーション部

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

        employeeView.DataSource = observableEmployee; // データバインド

        observableEmployee.Number = 100; // Number を変更。employeeView に反映されたかな?
        observableEmployee.Number = 200; // Number を再度変更。employeeView に反映されたかな?

        observableEmployee.Name = "福井太郎"; // Name を変更。employeeView に反映されたかな?
        observableEmployee.Name = "山田次郎"; // Name を再度変更。employeeView に反映されたかな?

        observableEmployee.Number = 200; // Number を変更していない。employeeView は更新されない筈
        observableEmployee.Name = "山田次郎"; // Name を変更していない。employeeView は更新されない筈

        // 念の為値を確認
        Console.WriteLine("employee - Number:{0}, Name:{1}", employee.Number, employee.Name);
        Console.WriteLine("observableEmployee - Number:{0}, Name:{1}", observableEmployee.Number, observableEmployee.Name);
    }
}
・実行結果

実行結果は予想通り。
この範囲ではうまく行っているようだ。

TextControl is updated: 100
TextControl is updated: 200
TextControl is updated: 福井太郎
TextControl is updated: 山田次郎
employee - Number:200, Name:山田次郎
observableEmployee - Number:200, Name:山田次郎

■ 今回のまとめ

今回は、DynamicObject を使ったアプローチを行った。

モデル部分がシンプルに書けるようになった。