DLR で名前付き引数を使う

C# で、メソッドに引数として「キーと値のペア」を渡す方法を考えてみます。
例えば HTTP で GET でアクセスするときに URL でクエリ文字列を指定する場合が挙げられます。

よく使われているのは、メソッドの引数に Dictionary や匿名型オブジェクトを渡す方法かと思います。
以下の HttpHelper クラスのように実装します。
なお、WebClient クラスではクエリ文字列 (QueryString プロパティ) は NameValueCollection 型であるため、
受け取った情報を NameValueCollection 型に変換しています。

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Net;
using System.Text;
namespace DynamicHttpConsole
{
public static class HttpHelper
{
public static string Get(string uri, object query) =>
query == null ? Get(uri) :
IsPropertiesObject(query) ? Get(uri, ToDictionary(query)) :
Get(uri, new { query });
static bool IsPropertiesObject(object o)
{
if (o == null) return false;
var oType = o.GetType();
return oType.IsClass && oType != typeof(string);
}
static IDictionary<string, object> ToDictionary(object obj) =>
TypeDescriptor.GetProperties(obj)
.Cast<PropertyDescriptor>()
.ToDictionary(p => p.Name, p => p.GetValue(obj));
public static string Get(string uri, IDictionary<string, object> query = null)
{
using (var web = new WebClient { Encoding = Encoding.UTF8 })
{
if (query != null)
web.QueryString = query.ToNameValueCollection();
return web.DownloadString(uri);
}
}
public static NameValueCollection ToNameValueCollection(this IDictionary<string, object> dictionary)
{
var collection = new NameValueCollection();
foreach (var item in dictionary)
collection[item.Key] = item.Value?.ToString();
return collection;
}
}
}
view raw HttpHelper.cs hosted with ❤ by GitHub
using System;
using System.Collections.Generic;
using System.Linq;
namespace DynamicHttpConsole
{
class Program
{
// http://zip.cgis.biz/
const string Uri_Cgis_Xml = "http://zip.cgis.biz/xml/zip.php";
static void Main(string[] args)
{
// No query
Console.WriteLine(HttpHelper.Get(Uri_Cgis_Xml));
// Dictionary
Console.WriteLine(HttpHelper.Get(Uri_Cgis_Xml, new Dictionary<string, object> { { "zn", "6048301" } }));
Console.WriteLine(HttpHelper.Get(Uri_Cgis_Xml, new Dictionary<string, object> { { "zn", "501" }, { "ver", 1 } }));
// Anonymous type
Console.WriteLine(HttpHelper.Get(Uri_Cgis_Xml, new { zn = "6050073" }));
Console.WriteLine(HttpHelper.Get(Uri_Cgis_Xml, new { zn = "502", ver = 1 }));
}
}
}
view raw Program.cs hosted with ❤ by GitHub

なお、ここでは題材として CGI’s 郵便番号検索 API を利用しています。

さて、動的言語ランタイム (DLR)名前付き引数を利用して、引数の情報を実行時に解決できないかと考えると、
次のような方法を思いつきます。

dynamic http = new DynamicHttpProxy();
var result = http.Get(Uri_Cgis_Xml, zn: "402", ver: 1);

実際、DynamicObject クラスを継承した DynamicHttpProxy クラスを次のように作れば可能です。

using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
namespace DynamicHttpConsole
{
public class DynamicHttpProxy : DynamicObject
{
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
if (binder.Name == "Get")
{
result = Get(binder.CallInfo.ArgumentNames, args);
return true;
}
return base.TryInvokeMember(binder, args, out result);
}
static string Get(ICollection<string> argNames, object[] args)
{
var query = argNames
.Zip(args.Skip(1), (n, v) => new { n, v })
.ToDictionary(_ => _.n, _ => _.v);
return HttpHelper.Get((string)args[0], query);
}
public string Get(string uri, object query = null) => HttpHelper.Get(uri, query);
}
}
view raw DynamicHttpProxy.cs hosted with ❤ by GitHub
using System;
using System.Collections.Generic;
using System.Linq;
namespace DynamicHttpConsole
{
class Program
{
// http://zip.cgis.biz/
const string Uri_Cgis_Xml = "http://zip.cgis.biz/xml/zip.php";
static void Main(string[] args)
{
dynamic http = new DynamicHttpProxy();
// No query
Console.WriteLine(http.Get(Uri_Cgis_Xml));
// Named arguments
Console.WriteLine(http.Get(Uri_Cgis_Xml, zn: "1510052"));
Console.WriteLine(http.Get(Uri_Cgis_Xml, zn: "402", ver: 1));
}
}
}
view raw Program.cs hosted with ❤ by GitHub

TryInvokeMember メソッドの中で、引数の名前は binder.CallInfo.ArgumentNames で取得できます。
ただし、引数の名前を指定せずに渡された分はここに含まれない (コレクションの長さが変わる) ため注意が必要です。

 

また、C# 7.0 で追加された ValueTuple を利用して、

var result = HttpHelper.Get(Uri_Cgis_Xml, (zn: "6050073"));

とする案もありましたが、

  • 要素が 1 つ以下の場合、タプル リテラルを記述できない
  • コンパイル後はフィールド名が残らないため、実行時に動的に取得できない

という制約により実現できませんでした。

次回:インターフェイスに対する透過プロキシ

作成したサンプル
DynamicHttpConsole (GitHub)

バージョン情報
C# 7.0
.NET Framework 4.5

参照
動的言語ランタイムの概要
名前付き引数と省略可能な引数
タプル – C# によるプログラミング入門

プロパティ変更をタイマーで同期して通知する

C# のコードでデータ バインディング (1) で書いた通り、
Binding Source は INotifyPropertyChanged か DependencyObject でなければ
プロパティ変更通知ができないため、永続的に値が反映される OneWay バインディングはできません。

そこで、プロパティ変更通知機能を持たない既存のオブジェクトをタイマーで監視して
プロパティ変更通知を発生させる DynamicSyncProxy<T> クラスというのを作ってみました。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Dynamic;
using System.Linq;
using System.Reflection;
using System.Threading;
namespace DynamicBindingWpf
{
public static class DynamicSyncProxy
{
public static DynamicSyncProxy<T> ToDynamicSyncProxy<T>(this T target, int intervalInMilliseconds = 1000) =>
new DynamicSyncProxy<T>(target, intervalInMilliseconds);
public static bool IsIndexer(this PropertyInfo property) =>
property.Name == "Item" && property.GetIndexParameters().Length > 0;
}
public class DynamicSyncProxy<T> : DynamicObject, INotifyPropertyChanged
{
T Target;
Dictionary<string, PropertyInfo> Properties;
Dictionary<string, object> PropertyValuesCache;
Timer SyncTimer;
public DynamicSyncProxy(T target, int intervalInMilliseconds = 1000)
{
Target = target;
Properties = Target.GetType().GetProperties()
.Where(p => !p.IsIndexer())
.ToDictionary(p => p.Name);
PropertyValuesCache = Properties.Values
.ToDictionary(p => p.Name, p => p.GetValue(Target));
SyncTimer = new Timer(o => SyncPropertyValues(), null, intervalInMilliseconds, intervalInMilliseconds);
}
public T GetTargetObject() => Target;
#region DynamicObject
public override IEnumerable<string> GetDynamicMemberNames()
{
return Properties.Keys;
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
result = Properties[binder.Name].GetValue(Target);
return true;
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
Properties[binder.Name].SetValue(Target, value);
return true;
}
#endregion
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propertyName) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
#endregion
#region Sync
void SyncPropertyValues()
{
foreach (var name in Properties.Keys)
SyncPropertyValue(name);
}
void SyncPropertyValue(string propertyName)
{
var oldValue = PropertyValuesCache[propertyName];
var newValue = Properties[propertyName].GetValue(Target);
if (Equals(oldValue, newValue)) return;
PropertyValuesCache[propertyName] = newValue;
NotifyPropertyChanged(propertyName);
}
#endregion
}
}
view raw DynamicSyncProxy.cs hosted with ❤ by GitHub

DynamicObject クラスを継承し、既存のオブジェクトを内部に持ちます。
そのオブジェクトのプロパティへのアクセスを透過するプロキシとして機能します。

さて、プロパティ変更通知機能を持たない TextModel クラスを用意して、
DynamicSyncProxy<T> を介してデータ バインディングを設定します (dynamic 型として扱います)。

using System;
namespace DynamicBindingWpf
{
public class AppModel
{
public dynamic TextModel { get; } = new TextModel().ToDynamicSyncProxy(300);
}
public class TextModel
{
string _Input;
public string Input
{
get { return _Input; }
set
{
_Input = value;
Output = _Input?.ToUpper();
}
}
public string Output { get; private set; }
}
}
view raw AppModel.cs hosted with ❤ by GitHub
<Window x:Class="DynamicBindingWpf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:DynamicBindingWpf"
Title="Dynamic Binding" Height="400" Width="600" FontSize="36">
<Window.DataContext>
<local:AppModel/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBox Text="{Binding TextModel.Input, UpdateSourceTrigger=PropertyChanged}" TextWrapping="Wrap"/>
<TextBlock Text="{Binding TextModel.Output}" TextWrapping="Wrap" Grid.Row="1"/>
</Grid>
</Window>
view raw MainWindow.xaml hosted with ❤ by GitHub

この例では 0.3 秒ごとに値が同期されます。

DynamicBinding

 

前回: ExpandoObject を使ったデータ バインディング
次回: XAML マークアップ拡張を自作する

作成したサンプル
DynamicBindingWpf (GitHub)

バージョン情報
C# 6.0
.NET Framework 4.5

参照
データ バインディングの概要
C# のコードでデータ バインディング (1)

ExpandoObject を使ったデータ バインディング

C# のコードでデータ バインディング (1) では、
INotifyPropertyChanged インターフェイスを実装した Person1 クラスに
プロパティを定義してデータ バインディングを構成しました。

ところで、dynamic 型として扱うことで実行時に任意のプロパティを追加できる ExpandoObject クラスというものがあり、
しかも INotifyPropertyChanged インターフェイスを実装しています。
したがって ExpandoObject クラスを使えば、
プロパティを事前に定義しなくてもデータ バインディングができるようになります。

次のコードは、C# のコードでデータ バインディング (1) の TwoWay バインディングのコードを少し書き換えて、
ExpandoObject クラスを使うようにしたものです。
Person1 クラスを使ったときと同様にデータ バインディングが動作します。

using System;
using System.Dynamic;
using System.Windows.Controls;
using System.Windows.Data;
namespace BindingConsole
{
class Program
{
[STAThread]
static void Main(string[] args)
{
// Binding Source.
dynamic person = new ExpandoObject();
person.Id = 123;
person.Name = "Taro";
// Binding Target must be FrameworkElement.
var textBox = new TextBox { Text = "Default" };
Console.WriteLine(textBox.Text); // Default
// Binds target to source.
var binding = new Binding(nameof(person.Name)) { Source = person, UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged };
textBox.SetBinding(TextBox.TextProperty, binding);
Console.WriteLine(textBox.Text); // Taro
// Changes source value.
person.Name = "Jiro";
Console.WriteLine(textBox.Text); // Jiro
// Changes target value.
textBox.Text = "Saburo";
Console.WriteLine(person.Name); // Saburo
}
}
}
view raw Program.cs hosted with ❤ by GitHub

 

さて、ExpandoObject は、XAML で DataContext に設定した場合でもプロパティにアクセスできます。
次に示す XAML では、空の ExpandoObject オブジェクトに対して、
UI コントロールから Input プロパティと Output プロパティに直接バインドしています。

<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ExpandoBindingWpf"
xmlns:Dynamic="clr-namespace:System.Dynamic;assembly=System.Core"
x:Class="ExpandoBindingWpf.MainWindow"
Title="Expando Binding" Height="400" Width="600" FontSize="36">
<Window.DataContext>
<Dynamic:ExpandoObject/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBox Text="{Binding Input, UpdateSourceTrigger=PropertyChanged}" TextWrapping="Wrap"/>
<TextBlock Text="{Binding Output}" TextWrapping="Wrap" Grid.Row="1"/>
</Grid>
</Window>
view raw MainWindow.xaml hosted with ❤ by GitHub
using System;
using System.ComponentModel;
using System.Windows;
namespace ExpandoBindingWpf
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
dynamic model = DataContext;
((INotifyPropertyChanged)model).PropertyChanged += (o, e) =>
{
if (e.PropertyName == "Input")
model.Output = model.Input?.ToUpper();
};
}
}
}
view raw MainWindow.xaml.cs hosted with ❤ by GitHub

コード側で、Input を大文字に変換して Output に反映させています。

ExpandoBinding

 

前回: C# のコードでデータ バインディング (3)
次回: プロパティ変更をタイマーで同期して通知する

作成したサンプル
BindingConsole (GitHub)
ExpandoBindingWpf (GitHub)

バージョン情報
C# 6.0
.NET Framework 4.5

参照
データ バインディングの概要
C# のコードでデータ バインディング (1)