かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

コンパイル時バインディング「x:Bind」の使い方

Windows Insider Preview 10074 + VS2015 RC時点の情報です。

正式リリース版で書き直しました

okazuki.hatenablog.com

x:Bind

今までのBindingが実行時に色々解決していたものを、x:BindというUWPで追加された新しいバインディングはコンパイル時にやってしまおうというものです。早いらしい。

x:Bindの簡単な使い方

普通のBindingと違ってDataSourceプロパティがありません。暗黙的にDataContextの値をとってくるというものでもありません。Pageで使うときはPageクラスのプロパティを指定できます。MVVMっぽく以下のようなViewModelクラスを作ったとします。

public class MainPageViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private static readonly PropertyChangedEventArgs NamePropertyChangedEventArgs = new PropertyChangedEventArgs(nameof(Name));

    private string name = "okazuki";

    public string Name
    {
        get { return this.name; }
        set
        {
            if (this.name == value) { return; }
            this.name = value;
            this.PropertyChanged?.Invoke(this, NamePropertyChangedEventArgs);
        }
    }

}

今まではDataContextにXAMLで指定したりコンストラクタでDataContextに設定したりしていましたが、コンパイル時バインディングではページのプロパティとして普通に定義するのがセオリーっぽいです。以下のような感じで。

/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page
{
    // ViewModelをプロパティとして定義する
    public MainPageViewModel ViewModel { get; } = new MainPageViewModel();

    public MainPage()
    {
        this.InitializeComponent();
    }
}

そうすると、画面のXAMLで以下のようにx:Bindを使ってバインディングできます。

<Page
    x:Class="App21.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App21"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <RelativePanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <TextBlock Text="{x:Bind ViewModel.Name}"
                   Style="{ThemeResource HeaderTextBlockStyle}"
                   RelativePanel.AlignHorizontalCenterWithPanel="True" />
    </RelativePanel>
</Page>

実行すると以下のような結果になります。

f:id:okazuki:20150509200959p:plain

デザイナ上では、以下のようにPathの中身が表示されます。

f:id:okazuki:20150509201130p:plain

イベントバインディング

このコンパイル時バインディングですが、イベントハンドラもバインドしてくれます。以下のようにイベントハンドラをページに作ります。

public sealed partial class MainPage : Page
{
    // ViewModelをプロパティとして定義する
    public MainPageViewModel ViewModel { get; } = new MainPageViewModel();

    public MainPage()
    {
        this.InitializeComponent();
    }

    // void ChangeName() でもOK
    public async void ChangeName(object sender, RoutedEventArgs e)
    {
        this.ViewModel.Name = "かずき";
        var dlg = new MessageDialog("名前を変えました");
        await dlg.ShowAsync();
    }
}

そして、XAMLでは以下のようにイベントとメソッドをx:Bindを使って紐づけます。

<Page
    x:Class="App21.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App21"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <RelativePanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <TextBlock x:Name="TextBlockName" 
                   Text="{x:Bind ViewModel.Name}"
                   Style="{ThemeResource HeaderTextBlockStyle}"
                   RelativePanel.AlignHorizontalCenterWithPanel="True" />
        
        <Button x:Name="ButtonChangeName"
                Content="ChangeName"
                Click="{x:Bind ChangeName}"
                RelativePanel.Below="TextBlockName" />
    </RelativePanel>
</Page>

実行してボタンを押すとダイアログが表示されてイベントのバインドが出来てることがわかります。

f:id:okazuki:20150509201748p:plain

バインディングのモード

イベントのバインドが出来ていて、ダイアログが出ているにも関わらず名前が書き変わっていません。これは、デフォルトのバインドのモードがOneTimeになっているためです。ViewModelのイベントの変更を反映したい場合はMode=OneWayを追加します。

<TextBlock x:Name="TextBlockName" 
           Text="{x:Bind ViewModel.Name, Mode=OneWay}"
           Style="{ThemeResource HeaderTextBlockStyle}"
           HorizontalAlignment="Center"
           RelativePanel.AlignLeftWithPanel="True"
           RelativePanel.AlignRightWithPanel="True" />

これで実行してボタンを押すと、Nameプロパティの変更がTextBlockにまで反映されます。

f:id:okazuki:20150509202044p:plain

TwoWayを指定すると双方向になります。TextBoxのTextプロパティとNameプロパティをバインドしてみます。

<Page
    x:Class="App21.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App21"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <RelativePanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <TextBlock x:Name="TextBlockName" 
                   Text="{x:Bind ViewModel.Name, Mode=OneWay}"
                   Style="{ThemeResource HeaderTextBlockStyle}"
                   RelativePanel.AlignHorizontalCenterWithPanel="True" />
        
        <Button x:Name="ButtonChangeName"
                Content="ChangeName"
                Click="{x:Bind ChangeName}"
                RelativePanel.Below="TextBlockName" />
        <!-- 双方向バインド -->
        <TextBox x:Name="TextBoxName"
                 Text="{x:Bind ViewModel.Name, Mode=TwoWay}"
                 HorizontalAlignment="Stretch"
                 RelativePanel.Below="ButtonChangeName"
                 RelativePanel.AlignRightWithPanel="True"
                 RelativePanel.AlignLeftWithPanel="True" />
    </RelativePanel>
</Page>

実行すると以下のようになります。LostFocusのタイミングで値がViewModelのNameに反映されるっぽい動きをしています。

f:id:okazuki:20150509202452p:plain

UpdateSourceTriggerというものは指定できなさそうなので、LoastFocus時に値が反映されるというのはどうしようもないっぽいです。

コレクションのバインディング

コレクションのバインディングもこれまでのBindingと同様に可能です。以下のような感じでコレクションのプロパティを持ったVMを作ります。

public class MainPageViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public ObservableCollection<Person> People { get; } = new ObservableCollection<Person>
    {
        new Person { Name = "okazuki" },
        new Person { Name = "kazuakix" },
        new Person { Name = "od_10z" },
    };
}

public class Person : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private static readonly PropertyChangedEventArgs NamePropertyChangedEventArgs = new PropertyChangedEventArgs(nameof(Name));

    private string name;

    public string Name
    {
        get { return this.name; }
        set
        {
            if (this.name == value) { return; }
            this.name = value;
            this.PropertyChanged?.Invoke(this, NamePropertyChangedEventArgs);
        }
    }

}

ItemsSourceでもx:Bindが使えます。

<Page
    x:Class="App21.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App21"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <RelativePanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <TextBlock x:Name="TextBlockTitle"
                   Text="ListView ItemsSource CompileTimeBinding"
                   Style="{ThemeResource HeaderTextBlockStyle}" 
                   RelativePanel.AlignHorizontalCenterWithPanel="True"/>

        <ListView x:Name="ListViewPeople"
                  ItemsSource="{x:Bind ViewModel.People}"
                  RelativePanel.Below="TextBlockTitle"
                  RelativePanel.AlignLeftWithPanel="True"
                  RelativePanel.AlignBottomWithPanel="True"
                  RelativePanel.AlignRightWithPanel="True">
        </ListView>
    </RelativePanel>
</Page>

実行すると、ちゃんとバインドされていることが確認できます。

f:id:okazuki:20150509203400p:plain

DataTemplate内でのバインド

このままだとToStringした結果がそのまま表示されるのでDataTemplateを適用します。DataTemplate内でもx:Bindは使えます。ただし、DataTemplateにx:DataTypeという属性を指定して何型が表示されているのかを明示的に指定する必要があります。

<ListView x:Name="ListViewPeople"
          ItemsSource="{x:Bind ViewModel.People}"
          RelativePanel.Below="TextBlockTitle"
          RelativePanel.AlignLeftWithPanel="True"
          RelativePanel.AlignBottomWithPanel="True"
          RelativePanel.AlignRightWithPanel="True">
    <ListView.ItemTemplate>
        <!-- x:DataTypeが必要 -->
        <DataTemplate x:DataType="local:Person">
            <RelativePanel>
                <TextBlock x:Name="TextBlockName" 
                           Text="{x:Bind Name}" 
                           Style="{ThemeResource BodyTextBlockStyle}"/>
            </RelativePanel>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

これで実行するとちゃんと名前が表示されます。

f:id:okazuki:20150509203803p:plain

DataTemplate内でのイベントのバインド

DataTemplate内でもイベントのバインドができます。Personクラスに以下のようなメソッドを追加します。

public class Person : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private static readonly PropertyChangedEventArgs NamePropertyChangedEventArgs = new PropertyChangedEventArgs(nameof(Name));

    private string name;

    public string Name
    {
        get { return this.name; }
        set
        {
            if (this.name == value) { return; }
            this.name = value;
            this.PropertyChanged?.Invoke(this, NamePropertyChangedEventArgs);
        }
    }

    public void ChangeName()
    {
        this.Name = "かずき";
    }
}

そして、ボタンをDataTemplate内に置いてChangeNameとバインドします。

<ListView x:Name="ListViewPeople"
          ItemsSource="{x:Bind ViewModel.People}"
          RelativePanel.Below="TextBlockTitle"
          RelativePanel.AlignLeftWithPanel="True"
          RelativePanel.AlignBottomWithPanel="True"
          RelativePanel.AlignRightWithPanel="True">
    <ListView.ItemTemplate>
        <!-- x:DataTypeが必要 -->
        <DataTemplate x:DataType="local:Person">
            <RelativePanel>
                <TextBlock x:Name="TextBlockName" 
                           Text="{x:Bind Name, Mode=OneWay}" 
                           Style="{ThemeResource BodyTextBlockStyle}"/>
                <Button x:Name="ButtonChangeName"
                        Content="ChangeName"
                        Click="{x:Bind ChangeName}"
                        RelativePanel.Below="TextBlockName" />
            </RelativePanel>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

実行してボタンを押すと、ちゃんとメソッドが呼ばれていることがわかります。

f:id:okazuki:20150509204250p:plain

UserControlをDataTemplateに使う場合

DataTemplateが複雑になってくるとUserControlに切り出すということはよくやると思います。そんな時は、UserControlに外部との値のやり取りをするためのプロパティを定義しておくと捗ります。

PersonFragmentという名前でUserControlを定義してViewModelという依存関係プロパティを定義しました。ChangeNameをViewModelに移譲するコードも追加しておきます。

public sealed partial class PersonFragment : UserControl
{

    public Person ViewModel
    {
        get { return (Person)GetValue(ViewModelProperty); }
        set { SetValue(ViewModelProperty, value); }
    }

    public static readonly DependencyProperty ViewModelProperty =
        DependencyProperty.Register("ViewModel", typeof(Person), typeof(PersonFragment), new PropertyMetadata(null));


    public PersonFragment()
    {
        this.InitializeComponent();
    }

    public void ChangeName()
    {
        this.ViewModel.ChangeName();
    }
}

XAMLは、以下のようになります。ViewModelのNameを表示するようにして、PersonFragmentに作成したChangeNameメソッドをボタンにバインドします。(現時点でイベントのバインドでViewModel.ChangeNameのように書くとコンパイルエラーになることがあるのでこうしてます。正式版ではViewModel.ChangeNameというパスが書けることを期待してたり)

<UserControl
    x:Class="App21.PersonFragment"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App21"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300"
    d:DesignWidth="400">
    <RelativePanel>
        <TextBlock x:Name="TextBlockName" 
                   Text="{x:Bind ViewModel.Name, Mode=OneWay}" 
                   Style="{ThemeResource BodyTextBlockStyle}"/>
        <Button x:Name="ButtonChangeName"
                Content="ChangeName"
                Click="{x:Bind ChangeName}"
                RelativePanel.Below="TextBlockName" />
    </RelativePanel>
</UserControl>

そして、DataTemplateを、このUserControlに差し替えます。そして、UserControlのViewModelにPersonをバインドします。

<ListView x:Name="ListViewPeople"
          ItemsSource="{x:Bind ViewModel.People}"
          RelativePanel.Below="TextBlockTitle"
          RelativePanel.AlignLeftWithPanel="True"
          RelativePanel.AlignBottomWithPanel="True"
          RelativePanel.AlignRightWithPanel="True">
    <ListView.ItemTemplate>
        <!-- x:DataTypeが必要 -->
        <DataTemplate x:DataType="local:Person">
            <local:PersonFragment ViewModel="{x:Bind}" />
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

実行すると元通り動いてることが確認できます。

f:id:okazuki:20150509205432p:plain

Converterもあるよ

ついでにConverterも従来通り使えますが、どうもApp.xamlに定義したConverterじゃないと実行時エラーになるみたいです。生成されているコードを見るとApplication.Current.Resourcesからコンバーターを取得していました。

こんなコンバータを作って

using System;
using Windows.UI.Xaml.Data;

namespace App21
{
    public class NameConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, string language)
        {
            return value + "さん";
        }

        public object ConvertBack(object value, Type targetType, object parameter, string language)
        {
            throw new NotImplementedException();
        }
    }
}

App.xamlにConverterを定義します。

<Application
    x:Class="App21.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App21"
    RequestedTheme="Light">
    <Application.Resources>
        <local:NameConverter x:Key="NameConverter" />
    </Application.Resources>
</Application>

そして、普通のBindingと同じようにConverterを指定します。

<UserControl
    x:Class="App21.PersonFragment"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App21"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300"
    d:DesignWidth="400">
    <RelativePanel>
        <TextBlock x:Name="TextBlockName" 
                   Text="{x:Bind ViewModel.Name, Mode=OneWay, Converter={StaticResource NameConverter}}" 
                   Style="{ThemeResource BodyTextBlockStyle}"/>
        <Button x:Name="ButtonChangeName"
                Content="ChangeName"
                Click="{x:Bind ChangeName}"
                RelativePanel.Below="TextBlockName" />
    </RelativePanel>
</UserControl>

実行すると以下のようになります。

f:id:okazuki:20150509205925p:plain

ちょっとしたテクニック

SelectedItemみたいなobject型のプロパティとPerson型のプロパティをx:Bindでバインドすると型が合わないといわれます。今回のアプリのListViewではPerson型をItemsSourceにバインドしてるので型変換の必要は無いにも関わらず怒られます。コンパイル時にはわからないんですね。

こんなプロパティをMainPageViewModelに生やして。

public class MainPageViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public ObservableCollection<Person> People { get; } = new ObservableCollection<Person>
    {
        new Person { Name = "okazuki" },
        new Person { Name = "kazuakix" },
        new Person { Name = "od_10z" },
    };

    private static readonly PropertyChangedEventArgs SelectedPersonPropertyChangedEventArgs = new PropertyChangedEventArgs(nameof(SelectedPerson));

    private Person selectedPerson;

    public Person SelectedPerson
    {
        get { return this.selectedPerson; }
        set
        {
            if (this.selectedPerson == value) { return; }
            this.selectedPerson = value;
            this.PropertyChanged?.Invoke(this, SelectedPersonPropertyChangedEventArgs);
        }
    }

}

そして、以下のようにXAMLでSelectedItemとTwoWayバインディングするとコンパイルエラーになります。

<ListView x:Name="ListViewPeople"
          ItemsSource="{x:Bind ViewModel.People}"
          SelectedItem="{x:Bind ViewModel.SelectedPerson, Mode=TwoWay}"
          RelativePanel.Below="TextBlockTitle"
          RelativePanel.AlignLeftWithPanel="True"
          RelativePanel.AlignBottomWithPanel="True"
          RelativePanel.AlignRightWithPanel="True">
    <ListView.ItemTemplate>
        <!-- x:DataTypeが必要 -->
        <DataTemplate x:DataType="local:Person">
            <local:PersonFragment ViewModel="{x:Bind}" />
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

回避策は何もしない以下のようなコンバーターを作って

using System;
using Windows.UI.Xaml.Data;

namespace App21
{
    public class ObjectConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, string language)
        {
            return value;
        }

        public object ConvertBack(object value, Type targetType, object parameter, string language)
        {
            return value;
        }
    }
}

App.xamlに登録して

<Application
    x:Class="App21.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App21"
    RequestedTheme="Light">
    <Application.Resources>
        <local:NameConverter x:Key="NameConverter" />
        <local:ObjectConverter x:Key="ObjectConverter" />
    </Application.Resources>
</Application>

SelectedItemのバインドに追加します。

<Page
    x:Class="App21.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App21"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <RelativePanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <TextBlock x:Name="TextBlockTitle"
                   Text="{x:Bind ViewModel.SelectedPerson.Name, Mode=OneWay}"
                   Style="{ThemeResource HeaderTextBlockStyle}" 
                   RelativePanel.AlignHorizontalCenterWithPanel="True"/>

        <ListView x:Name="ListViewPeople"
                  ItemsSource="{x:Bind ViewModel.People}"
                  SelectedItem="{x:Bind ViewModel.SelectedPerson, Mode=TwoWay, Converter={StaticResource ObjectConverter}}"
                  RelativePanel.Below="TextBlockTitle"
                  RelativePanel.AlignLeftWithPanel="True"
                  RelativePanel.AlignBottomWithPanel="True"
                  RelativePanel.AlignRightWithPanel="True">
            <ListView.ItemTemplate>
                <!-- x:DataTypeが必要 -->
                <DataTemplate x:DataType="local:Person">
                    <local:PersonFragment ViewModel="{x:Bind}" />
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </RelativePanel>
</Page>

実行するとばっちり動くようになります。

f:id:okazuki:20150509210822p:plain

個人的に解せぬ動き…。