[C#][Design Pattern] 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(() => 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))
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 = ((int)number).ToString());
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 を使った全然別のアプローチを試してみたい。

お楽しみに。