最近ようやくWinFormsからWPFに移行しようやという話になって、いろいろと調べている。
文字列をフォーマットして表示する(基本)
例えば、ViewModelのプロパティの値が__Title="坊ちゃん", Author="夏目漱石" である場合に、
画面上の項目にそのままバインドするのではなく、"「坊ちゃん」の作者は夏目漱石です"__ という形で表示したいという場合
MultiBinding
を使えばできる。
<TextBlock.Text>
<MultiBinding StringFormat="「{0}」の作者は{1}です">
<Binding Path="Title" />
<Binding Path="Author" />
</MultiBinding>
</TextBlock.Text>
OKOK。
多言語対応が必要であれば、リソースから引いた値をStringFormatに指定することもできる。
<!-- 以下のような形で、xmlns:pがPropertiesに割り当てられているものとする
xmlns:p="clr-namespace:MyNamespace.Properties" -->
<TextBlock.Text>
<MultiBinding StringFormat="{x:Static p:Resources.Author_of_0_is_1}">
<Binding Path="Title" />
<Binding Path="Author" />
</MultiBinding>
</TextBlock.Text>
できるんだけど、もっとこう楽に書けないか感あり。
マークアップ拡張で楽をする
マークアップ拡張を書けば、もっとシンプルに書けるようになりそう。たとえばこんな感じに。
<TextBlock Text="{my:Format {x:Static p:Resources.Author_of_0_is_1},
{Binding Title}, {Binding Author}}" />
で、できたっぽい。
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;
using System.Text.RegularExpressions;
namespace WonderfulPanda.WPF
{
public class FormatExtension : MarkupExtension
{
readonly object format_;
readonly object[] extensionArgs_;
public FormatExtension(object format, object arg1)
: this(format, new[] { arg1 })
{
}
public FormatExtension(object format, object arg1, object arg2)
: this(format, new[] { arg1, arg2 })
{
}
public FormatExtension(object format, object arg1, object arg2, object arg3)
: this(format, new[] { arg1, arg2, arg3 })
{
}
public FormatExtension(object format, object[] args)
{
if (!(format is string || format is BindingBase))
throw new ArgumentException("format must be string or binding", "format");
format_ = format;
extensionArgs_ = args;
}
public class BoundFormatConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter,
CultureInfo culture)
{
if (values.Length == 0)
throw new ArgumentException("values must have at least one element", "parameter");
var format = values[0].ToString();
try
{
switch (values.Length)
{
case 1:
return format;
case 2:
return string.Format(format, values[1]);
case 3:
return string.Format(format, values[1], values[2]);
case 4:
return string.Format(format, values[1], values[2], values[3]);
default:
return string.Format(format, values.Skip(1).ToArray());
}
}
catch (FormatException)
{
return "[FormatError]" + format;
}
}
public object[] ConvertBack(object value, Type[] targetTypes,
object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
private static IMultiValueConverter converter_ = new BoundFormatConverter();
public override object ProvideValue(IServiceProvider serviceProvider)
{
var mb = new MultiBinding() { Mode = BindingMode.OneWay };
if (format_ is BindingBase)
{
mb.Bindings.Add((BindingBase)format_);
mb.Converter = converter_;
}
else
{
mb.StringFormat = format_.ToString();
}
foreach (var arg in extensionArgs_)
{
var binding = (arg as BindingBase) ?? new Binding() { Source = arg };
mb.Bindings.Add(binding);
}
return mb.ProvideValue(serviceProvider);
}
}
}
書くのが楽になる以外のメリットとして、MultiBindingだとStringFormatにバインディングを指定することはできないけど、これだとできるという点がある。
実行中に動的に表示言語を切り替えたりしたい場合はその機能が生きてくるんじゃないかしら。
いろいろ
-
Bindingマークアップ拡張と同等の処理を行う拡張を自作する方法をぐぐると、自力で↓みたいなことをしてるサンプルがいくつか出てくるんだけど、その辺の処理は
binding.ProvideValue()
に移譲すべきだと思う、というかそうしないとちゃんと動かなかった。var pvt = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget)); BindingOperations.SetBinding((DependencyObject)pvt.TargetObject, (DependencyProperty)pvt.TargetProperty, binding);
-
マークアップ拡張のパーサに不具合があるらしく、自作のマークアップ拡張をそれを使うxamlと同じプロジェクトに入れるとネストを正しく解釈できなくなるらしい。知らずに少しハマった。
-
↓みたいな書き方ができるようなのも作ってみたけど、これは思ったほど便利にはならなかった。
インテリセン効かなくなっちゃうし。
<TextBlock Text="{my:Expand '「{Title}」の作者は{Author}です', Source=...}" />
何にせよまだまともにプロダクションで使ってないので、こういうのがどの程度需要があるのか分からない。
もっと筋のいい方法があるかもしれないし、お蔵入りするかもしれない。