銀の光と碧い空

クラウドなインフラとC#なアプリ開発の狭間にいるエンジニアの日々

Windows8.1 のストアプリで強化されたXAML データバインディングについて

XAML Advent Calendar 2013 - Adventar の5日目のエントリです。

今日はWindows 8.1のストアアプリで追加された機能の一つである、XAMLデータバインディングについて紹介します。強化といっても、どちらかというとWPFに近づいて、あの紫髪の人*1の怒りがややおさまるかも、という感じです。

大きく分けて3つあります。

FrameworkElement.DataContextChanged イベントの追加

Binding.FallbackValue プロパティと Binding.TargetNullValue プロパティ

2つまとめて紹介します。まずはサンプルコード。

<Grid x:Name="bindingData1" HorizontalAlignment="Left" Height="415" Margin="119,94,0,0" VerticalAlignment="Top" Width="406" DataContextChanged="bindingData1_DataContextChanged">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <Button Content="Bindingする" HorizontalAlignment="Center" Margin="10" Click="Button_Click"/>
            <Button Grid.Column="1" Content="正しくBindingする" HorizontalAlignment="Center" Margin="30,0,29,10" VerticalAlignment="Bottom" Click="Button_Click_1"/>
            <TextBlock Text="NullへのBinding" Grid.Row="1" Style="{StaticResource TitleTextBlockStyle}" Margin="10"/>
            <TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding NullValue, TargetNullValue='Nullだよ'}" Style="{StaticResource TitleTextBlockStyle}" Margin="10"/>
            <TextBlock Text="存在しないプロパティへのBinding" Grid.Row="2" Style="{StaticResource TitleTextBlockStyle}" Margin="10"/>
            <TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding NonExistingValue, FallbackValue='そんなプロパティないよ'}" Style="{StaticResource TitleTextBlockStyle}" Margin="10"/>
        </Grid>
using System.ComponentModel;
using System.Diagnostics;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace App
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            bindingData1.DataContext = new WrongBindingData1();
        }

        private void Button_Click_1(object sender, RoutedEventArgs e)
        {
            bindingData1.DataContext = new BindingData1()
            {
                NullValue = "Nullじゃないよ",
                NonExistingValue = "存在するよ"
            };
        }

        private void bindingData1_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
        {
            Debug.WriteLine(string.Format("DataContextが変わりました。対象のElemetは {0}、更新後のオブジェクトは {1}", sender.Name, args.NewValue));
        }
    }

    public class WrongBindingData1
    {
        public string NullValue { get; set; }

        public override string ToString()
        {
            return "WrongBindingData1";
        }
    }

    public class BindingData1
    {
        public string NullValue { get; set; }
        public string NonExistingValue { get; set; }

        public override string ToString()
        {
            return "BindingData1";
        }
    }
}

FrameworkElement.DataContextChanged イベント はその名の通り、DataContextへBindingするオブジェクトが変わったときに発火するイベントです。上のサンプルではデバッグ出力しています。 Binding.FallbackValue プロパティと Binding.TargetNullValue プロパティはBindingするプロパティが解決できなかったり、値がNullだったときに代わりにBindingする値を指定できます。

実際にNullだったりプロパティが存在しないオブジェクトと、プロパティの値が存在しているオブジェクトをBindingさせるとこんな風に表示されます。

f:id:tanaka733:20131204021730p:plain

f:id:tanaka733:20131204021732p:plain

また、このときデバッグ出力はこうなります。(途中で出るBindingのエラー出力は省略)

DataContextが変わりました。対象のElemetは bindingData1、更新後のオブジェクトは

DataContextが変わりました。対象のElemetは bindingData1、更新後のオブジェクトは WrongBindingData1

DataContextが変わりました。対象のElemetは bindingData1、更新後のオブジェクトは BindingData1

DataContextが変わりました。対象のElemetは bindingData1、更新後のオブジェクトは WrongBindingData1

DataContextが変わりました。対象のElemetは bindingData1、更新後のオブジェクトは BindingData1

Binding.UpdateSourceTrigger プロパティによるTextBoxのText プロパティの更新タイミングの制御

TextBoxのTextプロパティにプロパティをBindingしていると、デフォルトではTextBoxがフォーカスを失ったときにプロパティが更新されます。UpdateSourceTrigger プロパティを指定することで、TextBoxのTextの入力値が変化したらすぐプロパティを変更することや、外部から指定したタイミングでのみ更新できるようになりました。 後者は編集機能で、確定ボタンを押して初めて入力値を反映させたいときなのに有用そうです。

こちらもサンプル。

<StackPanel x:Name="UpdateSourceTriger1" HorizontalAlignment="Left" Height="203" Margin="624,94,0,0" VerticalAlignment="Top" Width="312" Orientation="Vertical">
            <TextBlock Text="規定の動作" Style="{StaticResource TitleTextBlockStyle}"/>
            <TextBox Margin="10" Text="{Binding Name, Mode=TwoWay}"/>
            <TextBlock Text="入力した値: " Margin="10"/>
            <TextBlock Text="{Binding Name}"/>
        </StackPanel>

        <StackPanel x:Name="UpdateSourceTriger2" HorizontalAlignment="Left" Height="203" Margin="624,297,0,0" VerticalAlignment="Top" Width="312" Orientation="Vertical">
            <TextBlock Text="PropertyChanged" Style="{StaticResource TitleTextBlockStyle}"/>
            <TextBox Margin="10" Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
            <TextBlock Text="入力した値: " Margin="10"/>
            <TextBlock Text="{Binding Name}"/>
        </StackPanel>

        <StackPanel x:Name="UpdateSourceTriger3" HorizontalAlignment="Left" Height="203" Margin="624,500,0,0" VerticalAlignment="Top" Width="312" Orientation="Vertical">
            <TextBlock Text="Explicit" Style="{StaticResource TitleTextBlockStyle}"/>
            <TextBox x:Name="nameTextBox3" Margin="10" Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=Explicit}"/>
            <TextBlock Text="入力した値: " Margin="10"/>
            <TextBlock Text="{Binding Name}"/>
            <Button Content="確定" Click="Button_Click_2"/>
        </StackPanel>
using System.ComponentModel;
using System.Diagnostics;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace App
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();

            UpdateSourceTriger1.DataContext = new UpdateSourceTriger();
            UpdateSourceTriger2.DataContext = new UpdateSourceTriger();
            UpdateSourceTriger3.DataContext = new UpdateSourceTriger();
        }

        private void Button_Click_2(object sender, RoutedEventArgs e)
        {
            nameTextBox3.GetBindingExpression(TextBox.TextProperty).UpdateSource();
        }
    }

    public class UpdateSourceTriger : INotifyPropertyChanged
    {
        private string name = "";
        public string Name
        {
            get
            {
                return name;
            }
            set
            {
                if (name == value)
                    return;

                name = value;
                if (this.PropertyChanged != null)
                    this.PropertyChanged(this, new PropertyChangedEventArgs("Name"));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }
}

実際に動かしてみてないと動きがわからないのですが、上から順にフォーカスを失ったとき、入力値が変わったとき、確定ボタンを押したとき、にTextBoxの入力値がしたのTextBlockに表示されることがわかります。 また、 UpdateSourceTrigger=Explicit のときは、 nameTextBox3.GetBindingExpression(TextBox.TextProperty).UpdateSource() (nameTextBox3 はTextBoxのインスタンス)を呼ぶことでViewからViewModelへプロパティ変更を通知させることができます。

というわけで、ストアアプリのXAMLもWPFに近づきつつある(気がする)ので、ぜひぜひXAMLの強力な機能でストアアプリを開発しましょう(きれいにまとめてつもり)

*1:XAMLアドベントカレンダーの作成者