BindingとConverter
WPFでVとVMをBindingするにあたって、SourceとTargetにそのままの型、そのままの値で流せばいい時はいいのだけれど、そうではない場合はConverterを指定する必要がある。
例えば、
- double値をThicknessに変換したい
- Sourceのbool値を反転したい
- FileSize(bytes)をKBに変換して表示したい
- 2つのSourceの値の合計をTargetにBindしたい
- 文字列をフォーマットして表示したい
などなど。
その都度専用のConverterを用意するのは面倒なので、汎用的に使えるものがほしい。
<TextBlock Text='{Binding Path=FileSize, Mode=OneWay, Converter={???},
ConverterParameter= v => (v / 1024).ToString() + \"KB\" }' />
<Panel Margin="{Binding Path=LeftMargin, Mode=OneWay, Converter={???},
ConverterParameter= v => new Thickness(v, 0, 0, 0) }" />
みたいな感じでlambdaを指定できるConverterがあれば捗るのではないかと思った。
実装
とはいえ、ConverterParameterに指定できるのは文字列でしかないので、それをパースして式木こねてDelegateを生成、とかはめんどくさいしやりたくない。
いっそIronPythonでも使うかー、という感じで試しに実装してみた。
radish.cs
using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Data;
using IronPython.Hosting;
using Microsoft.Scripting.Hosting;
namespace Radish
{
/// <summary>
/// Dynamic converter powered by IronPython
/// </summary>
public class PythonConverter : IValueConverter, IMultiValueConverter
{
readonly ScriptEngine _engine;
readonly ScriptScope _scope;
readonly Dictionary<string, Func<object, object>> _functions
= new Dictionary<string, Func<object, object>>();
public PythonConverter(string[] assemblies, string[] modules)
{
_engine = Python.CreateEngine();
_scope = _engine.CreateScope();
this.AddReferences(assemblies);
this.ImportModule(modules);
}
public void AddReferences(params string[] assemblies)
{
var script = new StringBuilder();
script.AppendLine("import clr");
foreach (var assembly in assemblies)
script.AppendFormat("clr.AddReference('{0}')", assembly).AppendLine();
_engine.Execute(script.ToString());
}
public void ImportModule(params string[] modules)
{
foreach (var module in modules)
_engine.Execute("import " + module, _scope);
}
public Func<object, object> DefineFunction(string source)
{
Func<object, object> func;
if (_functions.TryGetValue(source, out func))
return func;
var script = "lambda v:" + source;
var pyfunc = _engine.Execute(script, _scope);
func = _engine.Operations.ConvertTo<Func<object, object>>(pyfunc);
_functions[source] = func;
return func;
}
object IValueConverter.Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
if (parameter == null)
throw new NullReferenceException("parameter");
if (value == DependencyProperty.UnsetValue)
value = null;
try
{
var func = DefineFunction(parameter.ToString());
return func(value);
}
catch
{
return value;
}
}
object IValueConverter.ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
object IMultiValueConverter.Convert(object[] values, Type targetType,
object parameter, CultureInfo culture)
{
values = values.Select(v => v == DependencyProperty.UnsetValue ? null : v).ToArray();
return ((IValueConverter)this).Convert(values, targetType, parameter, culture);
}
object[] IMultiValueConverter.ConvertBack(object value, Type[] targetTypes,
object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
static PythonConverter _default =
new PythonConverter(new[] { "PresentationFramework",
"PresentationCore",
"WindowsBase" },
new[] { "System.Windows" });
public static PythonConverter Default
{
get { return _default; }
}
}
}
Dynamic ConverterだからRadish。
こんな感じで使う。
<TextBlock Text="{Binding Path=FileSize, Mode=OneWay,
Converter={x:Static PythonConverter.Default},
ConverterParameter= str(v // 1024) + \'KB\' }" />
<Panel Margin="{Binding Path=LeftMargin, Mode=OneWay,
Converter={x:Static PythonConverter.Default},
ConverterParameter= System.Windows.Thickness(v, 0, 0, 0) }" />
<!-- MultiBindingの場合 -->
<Setter Property="Clip">
<Setter.Value>
<RectangleGeometry>
<RectangleGeometry.Rect>
<MultiBinding Converter="{x:Static PythonConverter.Default}"
ConverterParameter="System.Windows.Rect(0, 0, v[0], v[1])" >
<Binding Path="ActualWidth" Source={TemplateParent} />
<Binding Path="ActualHeight" Source={TemplateParent} />
</MultiBinding>
</RectangleGeometry.Rect>
</RectangleGeometry>
</Setter.Value>
</Setter>
あとは適当なマークアップ拡張も用意すればもっとシンプルに書けるようになるはず。
結論
QuickConverter 使うといいと思う。