SE(たぶん)の雑感記

一応SEやっている筆者の思ったことを書き連ねます。会計学もやってたので、両方を生かした記事を書きたいと考えています。 でもテーマが定まってない感がすごい。

UWP挑戦記 その4 Bindingを使ったソースをx:Bindに変更してみた

今回は、UWP関連の話です。

UWPでも、WPFと同様MVVMモデルの考え方を踏襲しています。
WPFのデータバインドで書く、{Binding Path=xxxx}のような記法は、UWPでもそのまま使えます。

それはそうと、Windows10では、{x:Bind Path=xxxx}のような記法が、新たに追加されました。(ソースは以下)

docs.microsoft.com

何が違うの…?という疑問は当然です。私も思いました。
というわけで、実際使ってみて、何が違ったか、どこでつまづいたか、書いてみます。

今回は、まずアプリを{Binding}で動作させた後、{x:Bind}に変更していきます。
なお、Prismを利用しています。

Binding と x:Bind の違い

詳しくはこちら。

docs.microsoft.com

異なる部分の特徴をそれぞれ列挙すると、以下の通りです。

Binding

  • バインド先は、プロパティ名等の名前で解決
  • コンパイル時に、バインド先が無くても問題ない
  • データバインドは、実行時に動的に解決
  • ViewModelは、DataContextから自動的に取得
  • 型は動的解決(object型でも、指定したプロパティ等があればバインドされる)

x:Bind

  • バインド先は、プロパティ等の実態で解決
  • コンパイル時に、バインド先が無いとビルドエラーになる
  • データバインドは、ソースを自動生成して解決(そのため、Bindingより高速になり、デバッグ可能になる)
  • ViewModelは、Viewのソースにプロパティとして定義
  • 型は静的解決(Viewクラスが参照を解決できないと、ビルドエラー)

その他(片方にしか無いもの等)

  • x:Bindでは、UpdateSourceTriggerが削除
    AutoSuggestBoxを使え、ということでしょうか?
  • Modeの既定値変更
  • x:Bindでは、イベントもデータバインドできる

作成アプリ

こういうものです。

f:id:hiroronn:20170615080044p:plain

日付を選択すると、その日数差が下に表示されます。
日数差表示が適当なのは許して

開始終了の前後関係がおかしくなった場合、それぞれ同じ日に再設定、みたいな機能もあります。

Prismを使用していると書きましたが、DIコンテナを使って、ViewModelModelは注入しています。

ViewModelインターフェイスは以下のような感じです。*1

  • ViewModel
public interface IMainPageViewModel {
    /// <summary>
    /// 開始日
    /// </summary>
    DateTimeOffset FromDate { get; set; }

    /// <summary>
    /// 終了日
    /// </summary>
    DateTimeOffset ToDate { get; set; }

    string DateSpan { get; }
}

Viewの一部は、以下のような感じです。

<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

<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を継承する、というのはどうなんでしょう? まあ、大した手間ではないし、いいのですが、あまりインターフェイスの継承は…という気持ちはあります。

もっと複雑なケースがあるのでしょうが、それはまた課題にぶつかったら触れます。

では。

*1:DateTimeOffsetを使っているのは、DatePicker.Dateプロパティの型に合わせるため。Model側ではDateTime型になっている

*2:ViewModel.FromDate 等のViewModelという部分は、コードビハインドで定義したプロパティ名になる