今回は、UWP関連の話です。
UWPでも、WPFと同様MVVMモデル
の考え方を踏襲しています。
WPFのデータバインドで書く、{Binding Path=xxxx}
のような記法は、UWPでもそのまま使えます。
それはそうと、Windows10では、{x:Bind Path=xxxx}
のような記法が、新たに追加されました。(ソースは以下)
何が違うの…?という疑問は当然です。私も思いました。
というわけで、実際使ってみて、何が違ったか、どこでつまづいたか、書いてみます。
今回は、まずアプリを{Binding}
で動作させた後、{x:Bind}
に変更していきます。
なお、Prism
を利用しています。
Binding と x:Bind の違い
詳しくはこちら。
異なる部分の特徴をそれぞれ列挙すると、以下の通りです。
Binding
- バインド先は、プロパティ名等の名前で解決
- コンパイル時に、バインド先が無くても問題ない
- データバインドは、実行時に動的に解決
- ViewModelは、DataContextから自動的に取得
- 型は動的解決(object型でも、指定したプロパティ等があればバインドされる)
x:Bind
- バインド先は、プロパティ等の実態で解決
- コンパイル時に、バインド先が無いとビルドエラーになる
- データバインドは、ソースを自動生成して解決(そのため、Bindingより高速になり、デバッグ可能になる)
- ViewModelは、Viewのソースにプロパティとして定義
- 型は静的解決(Viewクラスが参照を解決できないと、ビルドエラー)
その他(片方にしか無いもの等)
- x:Bindでは、
UpdateSourceTrigger
が削除
AutoSuggestBox
を使え、ということでしょうか? - Modeの既定値変更
- x:Bindでは、イベントもデータバインドできる
作成アプリ
こういうものです。
日付を選択すると、その日数差が下に表示されます。
日数差表示が適当なのは許して
開始終了の前後関係がおかしくなった場合、それぞれ同じ日に再設定、みたいな機能もあります。
Prism
を使用していると書きましたが、DIコンテナを使って、ViewModel
とModel
は注入しています。
ViewModel
のインターフェイスは以下のような感じです。*1
- ViewModel
public interface IMainPageViewModel { /// <summary> /// 開始日 /// </summary> DateTimeOffset FromDate { get; set; } /// <summary> /// 終了日 /// </summary> DateTimeOffset ToDate { get; set; } string DateSpan { get; } }
View
の一部は、以下のような感じです。
- View(XAML)
<DatePicker x:Name="startDatePicker" RelativePanel.Below="startTextBlock" Date="{Binding FromDate, Mode=TwoWay}"/> <DatePicker x:Name="endDatePicker" RelativePanel.Below="endTextBlock" Date="{Binding ToDate, Mode=TwoWay}" /> <TextBlock x:Name="spanTextBlock" RelativePanel.Below="endDatePicker" Text="{Binding DateSpan, Mode=OneWay}"/>
x:Bindへ移行させる
ViewにViewModelのプロパティを追加する
DIコンテナ(Prism for Unity
)を利用しているため、ViewModelは自動的にDataContext
に設定されます。
なので、Viewでは、DataContext
の内容をキャストすればいいじゃん!と思っています。
- View(Code Behind)
public IMainPageViewModel ViewModel { get { if (this.DataContext != null) { return DataContext as IMainPageViewModel; } else { return null; } } }
例とかでは、インターフェイスではなく、具象クラスにキャスト(MainPageViewModel
とか)にキャストしています。
ただ、DIを使っているので、ここは流儀に合わせて、あえてインターフェイスにしました。
Viewのデータバインド書き換え
上で、{Binding FromDate}
と書いていたところを、{x:Bind ViewModel.FromDate}
のように書き換えます。
最初に書いた通り、{x;Bind}
はViewクラスが、データバインド先への参照を解決できる必要があるので、ViewModel.FromDate
のように、プロパティ式を書く必要があります。*2
- View(XAML)
<DatePicker x:Name="startDatePicker" RelativePanel.Below="startTextBlock" Date="{x:Bind ViewModel.FromDate, Mode=TwoWay}"/> <DatePicker x:Name="endDatePicker" RelativePanel.Below="endTextBlock" Date="{x:Bind ViewModel.ToDate, Mode=TwoWay}" /> <TextBlock x:Name="spanTextBlock" RelativePanel.Below="endDatePicker" Text="{x:Bind Path=ViewModel.DateSpan, Mode=OneWay}"/>
修正はこれだけです。
実行する
動かない…
なぜだ…
具体的には、実行して日付を変えても、何も起こらなくなりました。
日付を変えても、ViewModelのプロパティのセッターに入りません。
{Binding}
使用に戻すと、動作していました。
対処
ViewModelのインターフェイスを書き換えます。
*ViewModel(Interface)
public interface IMainPageViewModel :INotifyPropertyChanged { /// <summary> /// 開始日 /// </summary> DateTimeOffset FromDate { get; set; } /// <summary> /// 終了日 /// </summary> DateTimeOffset ToDate { get; set; } string DateSpan { get; } }
INotifyPropertyChanged
インターフェイスを継承するようにしています。
その後はおなじないですが…
なんでもいいので、Viewのデザイナを書き換えます。
すると、データバインドが動作するようになります。
対処法の理由
先述の通り、{x:Bind}
では、
データバインドは、ソースを自動生成して解決
されます。
そして、データバインドの自動解決ソースが生成されるには、
バインドしている先が、INotifyPropertyChanged
を実装している
ことが、条件の一つとなっているようです。
ViewModelの具象クラスは、言うまでもなく実装済みですので、あまり関係ありません。
デザイナを書き換えた理由は、もう一度、自動生成ソースを作り直させるためです。
移行を終えて
これで、1時間半ぐらい詰まりました。
でも、意味は分かったので良かったです。
ただ、ViewModelのインターフェイスでINotifyPropertyChagned
を継承する、というのはどうなんでしょう?
まあ、大した手間ではないし、いいのですが、あまりインターフェイスの継承は…という気持ちはあります。
もっと複雑なケースがあるのでしょうが、それはまた課題にぶつかったら触れます。
では。