Windows Insider Preview 10074 + VS2015 RC時点の情報です。
x:Bind
今までのBindingが実行時に色々解決していたものを、x:BindというUWPで追加された新しいバインディングはコンパイル時にやってしまおうというものです。早いらしい。
x:Bindの簡単な使い方
普通のBindingと違ってDataSourceプロパティがありません。暗黙的にDataContextの値をとってくるというものでもありません。Pageで使うときはPageクラスのプロパティを指定できます。MVVMっぽく以下のようなViewModelクラスを作ったとします。
publicclass MainPageViewModel : INotifyPropertyChanged { publicevent PropertyChangedEventHandler PropertyChanged; privatestaticreadonly PropertyChangedEventArgs NamePropertyChangedEventArgs = new PropertyChangedEventArgs(nameof(Name)); privatestring name = "okazuki"; publicstring Name { get { returnthis.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>publicsealedpartialclass MainPage : Page { // ViewModelをプロパティとして定義するpublic MainPageViewModel ViewModel { get; } = new MainPageViewModel(); public MainPage() { this.InitializeComponent(); } }
そうすると、画面のXAMLで以下のようにx:Bindを使ってバインディングできます。
<Pagex: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>
実行すると以下のような結果になります。
デザイナ上では、以下のようにPathの中身が表示されます。
イベントバインディング
このコンパイル時バインディングですが、イベントハンドラもバインドしてくれます。以下のようにイベントハンドラをページに作ります。
publicsealedpartialclass MainPage : Page { // ViewModelをプロパティとして定義するpublic MainPageViewModel ViewModel { get; } = new MainPageViewModel(); public MainPage() { this.InitializeComponent(); } // void ChangeName() でもOKpublic async void ChangeName(object sender, RoutedEventArgs e) { this.ViewModel.Name = "かずき"; var dlg = new MessageDialog("名前を変えました"); await dlg.ShowAsync(); } }
そして、XAMLでは以下のようにイベントとメソッドをx:Bindを使って紐づけます。
<Pagex: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>
実行してボタンを押すとダイアログが表示されてイベントのバインドが出来てることがわかります。
バインディングのモード
イベントのバインドが出来ていて、ダイアログが出ているにも関わらず名前が書き変わっていません。これは、デフォルトのバインドのモードが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にまで反映されます。
TwoWayを指定すると双方向になります。TextBoxのTextプロパティとNameプロパティをバインドしてみます。
<Pagex: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に反映されるっぽい動きをしています。
UpdateSourceTriggerというものは指定できなさそうなので、LoastFocus時に値が反映されるというのはどうしようもないっぽいです。
コレクションのバインディング
コレクションのバインディングもこれまでのBindingと同様に可能です。以下のような感じでコレクションのプロパティを持ったVMを作ります。
publicclass MainPageViewModel : INotifyPropertyChanged { publicevent PropertyChangedEventHandler PropertyChanged; public ObservableCollection<Person> People { get; } = new ObservableCollection<Person> { new Person { Name = "okazuki" }, new Person { Name = "kazuakix" }, new Person { Name = "od_10z" }, }; } publicclass Person : INotifyPropertyChanged { publicevent PropertyChangedEventHandler PropertyChanged; privatestaticreadonly PropertyChangedEventArgs NamePropertyChangedEventArgs = new PropertyChangedEventArgs(nameof(Name)); privatestring name; publicstring Name { get { returnthis.name; } set { if (this.name == value) { return; } this.name = value; this.PropertyChanged?.Invoke(this, NamePropertyChangedEventArgs); } } }
ItemsSourceでもx:Bindが使えます。
<Pagex: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>
実行すると、ちゃんとバインドされていることが確認できます。
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>
これで実行するとちゃんと名前が表示されます。
DataTemplate内でのイベントのバインド
DataTemplate内でもイベントのバインドができます。Personクラスに以下のようなメソッドを追加します。
publicclass Person : INotifyPropertyChanged { publicevent PropertyChangedEventHandler PropertyChanged; privatestaticreadonly PropertyChangedEventArgs NamePropertyChangedEventArgs = new PropertyChangedEventArgs(nameof(Name)); privatestring name; publicstring Name { get { returnthis.name; } set { if (this.name == value) { return; } this.name = value; this.PropertyChanged?.Invoke(this, NamePropertyChangedEventArgs); } } publicvoid 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>
実行してボタンを押すと、ちゃんとメソッドが呼ばれていることがわかります。
UserControlをDataTemplateに使う場合
DataTemplateが複雑になってくるとUserControlに切り出すということはよくやると思います。そんな時は、UserControlに外部との値のやり取りをするためのプロパティを定義しておくと捗ります。
PersonFragmentという名前でUserControlを定義してViewModelという依存関係プロパティを定義しました。ChangeNameをViewModelに移譲するコードも追加しておきます。
publicsealedpartialclass PersonFragment : UserControl { public Person ViewModel { get { return (Person)GetValue(ViewModelProperty); } set { SetValue(ViewModelProperty, value); } } publicstaticreadonly DependencyProperty ViewModelProperty = DependencyProperty.Register("ViewModel", typeof(Person), typeof(PersonFragment), new PropertyMetadata(null)); public PersonFragment() { this.InitializeComponent(); } publicvoid ChangeName() { this.ViewModel.ChangeName(); } }
XAMLは、以下のようになります。ViewModelのNameを表示するようにして、PersonFragmentに作成したChangeNameメソッドをボタンにバインドします。(現時点でイベントのバインドでViewModel.ChangeNameのように書くとコンパイルエラーになることがあるのでこうしてます。正式版ではViewModel.ChangeNameというパスが書けることを期待してたり)
<UserControlx: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>
実行すると元通り動いてることが確認できます。
Converterもあるよ
ついでにConverterも従来通り使えますが、どうもApp.xamlに定義したConverterじゃないと実行時エラーになるみたいです。生成されているコードを見るとApplication.Current.Resourcesからコンバーターを取得していました。
こんなコンバータを作って
using System; using Windows.UI.Xaml.Data; namespace App21 { publicclass NameConverter : IValueConverter { publicobject Convert(objectvalue, Type targetType, object parameter, string language) { returnvalue + "さん"; } publicobject ConvertBack(objectvalue, Type targetType, object parameter, string language) { thrownew NotImplementedException(); } } }
App.xamlにConverterを定義します。
<Applicationx: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を指定します。
<UserControlx: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>
実行すると以下のようになります。
ちょっとしたテクニック
SelectedItemみたいなobject型のプロパティとPerson型のプロパティをx:Bindでバインドすると型が合わないといわれます。今回のアプリのListViewではPerson型をItemsSourceにバインドしてるので型変換の必要は無いにも関わらず怒られます。コンパイル時にはわからないんですね。
こんなプロパティをMainPageViewModelに生やして。
publicclass MainPageViewModel : INotifyPropertyChanged { publicevent PropertyChangedEventHandler PropertyChanged; public ObservableCollection<Person> People { get; } = new ObservableCollection<Person> { new Person { Name = "okazuki" }, new Person { Name = "kazuakix" }, new Person { Name = "od_10z" }, }; privatestaticreadonly PropertyChangedEventArgs SelectedPersonPropertyChangedEventArgs = new PropertyChangedEventArgs(nameof(SelectedPerson)); private Person selectedPerson; public Person SelectedPerson { get { returnthis.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 { publicclass ObjectConverter : IValueConverter { publicobject Convert(objectvalue, Type targetType, object parameter, string language) { returnvalue; } publicobject ConvertBack(objectvalue, Type targetType, object parameter, string language) { returnvalue; } } }
App.xamlに登録して
<Applicationx: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のバインドに追加します。
<Pagex: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>
実行するとばっちり動くようになります。
個人的に解せぬ動き…。