[WPF]マルチバインディングで複数プロパティを繋げて、一つのTextBlockに表示する


やりたいこと

今まで、ビューモデルに作った単体のプロパティを画面のコントロールにバインドして、表示させたり入力を受けたりしていたが、例えば複数のstringのプロパティをつなげて、一つの文字列にして表示したいときに、複数のプロパティを一つのコントロールにバインドするようなことがしたい。
その他にも、下記のような場合に対応したい。

  • 複数のstringのプロパティをつなげて、一つの文字列にして表示したい
  • 複数のboolのプロパティを見て、コントロールの有効・無効や表示・非表示を判定したい
  • など...

実験内容

このような画面にして、以下のようなことを試す。

  • ListBoxの中身をItemTemplateでカスタムする。
  • ListBoxの中身は、マルチバインディングで作成したテキストを表示する。
  • そのテキストは、マルチバインディングした複数の項目を、コンバーターでつなげたものにする。
  • マルチバインディングするのは、ListBoxのItemSourceにバインドされたリストと、ビューモデルのプロパティ、にする。(イメージとしては、リストの中身個別の値に、全体で持ってる情報をくっつける、というイメージ)

やり方(全体コード)

画面xaml(View)

MainWindows.xaml
<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800"
        Name="Root">

    <!-- ViewModelを関連付けする -->
    <Window.DataContext>
        <local:ViewModel/>
    </Window.DataContext>

    <!-- コンバーター -->
    <Window.Resources>
        <local:MyMultiStringConverter x:Key="MyMultiStringConverter"/>
    </Window.Resources>

    <!-- 画面表示 -->
    <Grid>
        <ListBox ItemsSource="{Binding ItemList}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock>
                        <TextBlock.Text>
                            <MultiBinding Converter="{StaticResource MyMultiStringConverter}">
                                <!-- ★values[0] : リストのItemSourceの項目にバインド -->
                                <Binding/>
                                <!-- ★values[1] : ViewModelのプロパティにバインド -->
                                <Binding RelativeSource="{RelativeSource FindAncestor, AncestorType=Window}" Path="DataContext.Unit"/>
                                <!--<Binding Path="DataContext.Unit" ElementName="Root"/>--><!-- これでもOK -->
                                <!-- ★values[2] : コードビハインドのプロパティにバインド -->
                                <Binding Path="CodeBehindProp" ElementName="Root"/>
                            </MultiBinding>
                        </TextBlock.Text>
                    </TextBlock>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</Window>

画面コードビハインド(View)

MainWindows.xaml.cs
using System.Windows;

namespace WpfApp1
{
    public partial class MainWindow : Window
    {
        public string CodeBehindProp { get; } = "★";

        public MainWindow()
        {
            InitializeComponent();
        }
    }
}

コンバーター(View)

Converter.cs
using System;
using System.Globalization;
using System.Windows.Data;

namespace WpfApp1
{
    internal class MyMultiStringConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            int val = (int)values[0];           // ★xamlのMultiBindingで、1つ目に入れたもの
            string unit = (string)values[1];    // ★xamlのMultiBindingで、2つ目に入れたもの
            string behind = (string)values[2];  // ★xamlのMultiBindingで、3つ目に入れたもの

            return val.ToString() + unit + behind;
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}


ViewModel

ViewModel.cs
using System.Collections.ObjectModel;

namespace WpfApp1
{
    class ViewModel : BindingBase
    {        
        public ObservableCollection<int> ItemList { get => _itemList; set => _itemList = value; }
        private ObservableCollection<int> _itemList = new ObservableCollection<int>();

        public string Unit { get; } = "メートル";

        public ViewModel()
        {
            // 実験用にリストを作成
            ItemList.Add(1);
            ItemList.Add(2);
            ItemList.Add(3);
            ItemList.Add(4);
            ItemList.Add(5);
        }
    }
}

画面のxamlで、以下のことをして、複数プロパティから一つの文字列を作っている。

  • TextBlockに、マルチバインディングに使うコンバーター(MyMultiStringConverter)を指定する
  • MultiBindingの中に、Bindingするものを順番に指定する(Binding・・・)
  • コンバーターの方で、Bindingしたものを受け取って、一つの文字列につなげる
MainWindow.xaml
<TextBlock.Text>
    <MultiBinding Converter="{StaticResource MyMultiStringConverter}">
        <!-- ★values[0] : リストのItemSourceの項目にバインド -->
        <Binding/>
        <!-- ★values[1] : ViewModelのプロパティにバインド -->
        <Binding RelativeSource="{RelativeSource FindAncestor, AncestorType=Window}" Path="DataContext.Unit"/>
        <!-- ★values[2] : コードビハインドのプロパティにバインド -->
        <Binding Path="CodeBehindProp" ElementName="Root"/>
    </MultiBinding>
</TextBlock.Text>

コンバーターの方では、上のxamlの<MultiBinding>の中に並べた<Binding...>が、valuesの配列として順番に渡されてくる。
今回は、それを順番に受けてつなげてから、一つの文字列として返している。
(それが、MultiBindingした結果として、TextBlockに表示される)

Converters.cs
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
    int val = (int)values[0];           // ★xamlのMultiBindingで、1つ目に入れたもの
    string unit = (string)values[1];    // ★xamlのMultiBindingで、2つ目に入れたもの
    string behind = (string)values[2];  // ★xamlのMultiBindingで、3つ目に入れたもの

    return val.ToString() + unit + behind;// ★これが、TextBlockに表示される
}

備考

今回は、MultiBindingしたものを一つの文字列につなげる、ということをしたが、
TextBlockではなく、例えば<Button>のIsEnabledにMultiBindingして、複数のboolを受けてorとかandをとって、一個でもフラグ立ってたら/全部のフラグが立ってたらボタン有効、などできそう。

また、今回やってることは、マルチバインディングを使わなくても、下記のxamlのように普通のBindingでできてしまうが、ここにConverterの中でなにか計算したいとかになったときに、MultiBindingが便利なのではないかと思う。

同じことが普通のBindingでできる例.xaml
<Grid>
    <ListBox ItemsSource="{Binding ItemList}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding}"/>
                    <TextBlock Text="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=Window}, Path=DataContext.Unit}"/>
                    <TextBlock Text="{Binding CodeBehindProp, ElementName=Root}"/>
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Grid>