じょいのーと

プログラミング関連のアレコレを書いています

MultiBindingの使い方と使いどころ

MultiBindingは複数Sourceと一つのTargetをバインディングするクラスです。
便利なんですが、あまりWeb上に日本語情報がないので健忘録を兼ねて書いてみます。

通常のBindingとの違いは、主に3点。

1. 複数のBindingSourceを指定可能

Multiというからには当然ですが、何個でも指定できます。通常のBinding複数個を束ねるようなイメージをすれば大体は合ってます。

2. 複数Sourceの変更通知をトリガーに動作する

1にも書きましたが、通常のBindingを束ねたようなクラスなので、束ねられたN個のBindingが1つでも反応した時点でMultiBindingSourceによるTargetの更新(あるいは逆)が行われます。

3. MultiValueConverterの指定が必須

複数のSourceをどういう形でTargetに反映させますか? という部分を書く必要があります。
通常のBindingの場合、BindingSourceとTargetが1対1なので単純な型変換と代入で済みますが、Sourceが複数の場合は"標準の動き"というものを定義できないので、IMultiValueConverterインターフェイスを実装したオブジェクトを指定する必要があります。(※しないと例外が発生します)

MultiBindingの記法

まずは書き方です。
VS2013の時点で、MultiBindingは入力補完に出てくるパターンがない(と思う)ので不安になりますが、書けば動きます。

通常のBindingはXAMLで以下のように書きます。

<!-- TargetTextプロパティとTextBlock.Textプロパティの値をBinding -->
<TextBlock Text="{Binding SourceText}"/>

<!-- あるいは、省略ナシで書くと以下 -->
<TextBlock>
    <TextBlock.Text>
        <Binding Path="SourceText"/>
    </TextBlock.Text>
</TextBlock>

MultiBindingだとXAMLで以下のように書きます。

<TextBlock>
    <TextBlock.Text>
        <MultiBinding Converter="{StaticResource xxxConverter}">
            <Binding Path="SourceText1"/>
            <Binding Path="SourceText2"/>
        </MultiBinding>
    </TextBlock.Text>
</TextBlock>

MultiValueConverterの実装例

すごいざっくりした例を出すと、以下のような感じがイメージつかみやすいかと思います。

実装方法
public class xxxConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return values.Aggregate((lhs, rhs) => String.Format("{0}:{1}", lhs, rhs));
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        return value.ToString().Split(':');
    }
}

object配列に入ってる順はXAMLの記述順なので、取扱注意。
想定と違う値を入れるとすぐ落ちますが、動作イメージ優先で。

使用方法
<Window.Resources>
    <converter:xxxConverter x:Key="xxxConverter" />
</Window.Resources>
<StackPanel>
    <TextBox>
        <TextBox.Text>
            <MultiBinding Converter="{StaticResource xxxConverter}" UpdateSourceTrigger="PropertyChanged">
                <Binding Path="A" />
                <Binding Path="B" />
                <Binding Path="C" />
            </MultiBinding>
        </TextBox.Text>
    </TextBox>
    <TextBox Text="{Binding A, UpdateSourceTrigger=PropertyChanged}" />
    <TextBox Text="{Binding B, UpdateSourceTrigger=PropertyChanged}" />
    <TextBox Text="{Binding C, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>

A,B,CはDataContextに設定されているクラスのプロパティ(string型)だと思って読んでください。
こんな感じのものを動かすと、MultiValueの相互Bindingがどういう事をやっているかのイメージが付きやすいと思います。

普段はあまり使いませんが、2つの情報ソースの合成結果を表示したい場合に便利です。

非同期Bindingの動きについて

WPFのBindingを行う際に非同期指定を行うと値を非同期でBindingするように出来ますが、その挙動についてメモ。

Binding.IsAsyncプロパティ

XAMLに以下のようにIsAsync=True指定をすると、Bindingが非同期化されます。

<TextBlock Text="{Binding Hoge, IsAsync=True}" />

こいつが何者かについてはMSDNBinding.IsAsync プロパティ (System.Windows.Data)があるのでそこを見ればいいとして、具体的にどういう挙動をするのかについて簡単に書いてみます。

非同期BindingのBinding結果解決手順

  1. ThreadPoolからThreadを取得し、取得したThread上でBindingで指定するPathに存在するプロパティのgetterから値を取得する
  2. Bindingオブジェクトが取得した値をDispatcher.InvokeでUIスレッドへと転送する
  3. ValueConverter等で値の変換が行われる
  4. UI要素上に表示

以上の様な手順で、IsAsync指定したBindingは解決されます。

同期Bindingとの違い

同期的なBindingとの大きな違いとしては、Binding先プロパティへのアクセスを行うスレッドがUIスレッドではなくなる点です。
通常、XAML上で記述されたBindingはsetもgetもUIスレッド上で全て行われますが、IsAsync=True指定を行った瞬間にgetに関してはスレッドプール上のバックグラウンドスレッドで行われるようになります。

気をつけるべきこと

気をつけるべきこととして、実行スレッド依存の問題とパフォーマンスの問題があります。

1.実行スレッド依存の問題

WPFのUI要素はUIスレッド上からしかアクセス出来ないようになっているので困る系の問題です。
MVVMで作っていても、ViewModel層にUI要素(特にBitmapImageとか)を持たざるを得ない設計もあると思いますが、そういった場合には実行スレッドが切り替わると困ります。
例えば「初回はBitmap作ってFreezeして、次回以降はキャッシュを返す」みたいな動きを実装している時にIsAsyncを使ってしまうとBitmap生成のスレッドがUIスレッドでなくなるので例外が発生してBitmapImage作れない…などですね。
対処法としてはバックグラウンド スレッドで UI 要素を作るとメモリリークする (WPF) | grabacr.nétバックグラウンドスレッドでUI要素を作るともっと問題は深刻かもしれない。(WPF) | LOGarithm付近で紹介されている地雷を避けつつ、STA指定したThread上でUI要素を作るとgetterの実行スレッドは気にしないでも大丈夫になります。

2.パフォーマンスの問題

こっちの問題は単純で、一つのgetterに対して1つのThreadを使って値の取得&転送を行うことになるため、単純に処理のオーバーヘッド分だけ遅いです。
上で紹介したMSDNのページ内でも言及されていますが、そもそもgetterで時間のかかる処理しないで、どこか別のタイミングで値を生成してキャッシュしておけ…というのを基本方針にした方が無難です。
手元で数千個のBindingを全てIsAsync=Trueにしてみた感覚だと、値の取得タイミングとは別に総計で10%くらいの性能が劣化するようです。

ピクセル単位スクロール可能な仮想化ListBox

お仕事でList系のControlの描画コストが高すぎてパフォーマンスが出ないと悩んでた部分が、趣味コード弄ってる時に解決できる事が分かったのでメモ。

WPF仮想化の基本

基本的にListBox/ListViewは初期状態で仮想化がONになっています。
なので、基本的にはそのまま何も追加のコードを書かずに仮想化されます。

しかし、デフォルトの仮想化はアイテム単位での仮想化を行っている関係で、ListBoxのアイテムが中途半端な状態(半分だけ見えてるなど)を許しません。

UI的にそれが許される場合は良いのですが、1つ1つのItemが大きい場合やスクロールした事が分かりづらい外観の場合には致命的にUXが悪化します。

そのため、Web上で見つかる多くの資料では以下のようなXAMLを書けと書いてあります。

<ListBox ItemsSource="{Binding Collection}"
         ScrollViewer.CanContentScroll="False"/>

ListBoxが内部的に所有しているScrollViewerコントロールのCanContentScroll(Content単位スクロールフラグ)をFalseにしろ、という記述を追加してます。

これでピクセル単位のスクロールが可能になるんですが、CanContentScrollをFalseにした場合、仮想化はOFFになります。つまり、超重いということ。

これは内部的な仕組みとして、コンテンツ単位で仮想化している為に回避できない巨大なデメリットでした。

WPF仮想化の基本(.Net Framework 4.5版)

実は上記コードは.Net Framework 4.0の世界では正しいのですが、現在のWPFの状況には合ってません。

実は.Net Framework 4.5から、ListBoxが内部的に持つ仮想化パネルのスクロールオプションをPixel単位に切り替えるプロパティが追加されてます。

これにより、ピクセル単位スクロール+仮想化ONが簡単に実現可能になりました。

2014年10月現在、ListBoxでピクセル単位スクロールをしたいならば、以下のように記述するのが最善です。

<ListBox ItemsSource="{Binding Collection}"
         VirtualizingPanel.ScrollUnit="Pixel"/>

注意点

基本的にWPFを扱っている時には明示的に仮想化しなくても勝手に仮想化してくれるんですが、以下のような場合には仮想化されないので気をつけてください。

1.仮想化したいListBoxがStackPanelの上に乗っている

StackPanelやWrapPanel、ListBox等の上にListBoxが乗っかっている場合には仮想化は強制的にOFFになります。

GridやBorder等の固定的なコントロールの上に乗っている必要があります。

2.CanContentScroll="False"している

VirtualizingPanel.ScrollUnit="Pixel"を指定しても、以下のようにすると仮想化"だけ"OFFになります。

<ListBox ItemsSource="{Binding Collection}"
         VirtualizingPanel.ScrollUnit="Pixel"
         ScrollViewer.CanContentScroll="False"/><!-- 念のため は ダメ、ゼッタイ -->

念のため、とか言いながら記述すると仮想化がOFFになります。