【Xamarin.Forms】ListViewのデータソースが変化したら自動でUIが更新されるようにする


前回、ListViewをXAMLで定義する場合の最小限のコードを書きました。

【Xamarin.Forms】ListView の CustomCell を XAML で定義する

上記の記事でListViewとその中身のCustomViewCellをXAMLで定義できるようになりましたが、これではまだデータソースに変更があった場合にUIが更新されません。

そこで、今回はデータソースの変更に合わせて自動でUIが更新されるよう修正を加えたいと思います。

参考

今回は以下の記事を全面的に参考にしています。

ListViewで学ぶコレクションのBind通知 - @furugen

上記の記事との差分としては、getterにラムダを使っていることと、nullチェックを今風の書き方にしているくらいかと思います。

本文

事前準備

まずはデータソースを任意のタイミングで変更できるよう、以下2種類のボタンを追加しておきます。

  • 1件目のデータのTitleを変更するボタン
  • データソースの末尾にデータを追加するボタン

XAMLにボタンを2つ追加し、StackLayoutでレイアウトを整えます。1

MainPage.xaml
<StackLayout
        Orientation="Vertical"
        Margin="16"
        Spacing="16">

        <StackLayout
            Orientation="Horizontal"
            Spacing="16">

            <Button
                HorizontalOptions="FillAndExpand"
                HeightRequest="60"
                BackgroundColor="Navy"
                TextColor="White"
                Text="Change Title"
                Clicked="ChangeFirstTitle"/>

            <Button
                HorizontalOptions="FillAndExpand"
                HeightRequest="60"
                BackgroundColor="Teal"
                TextColor="White"
                Text="Append Data"
                Clicked="AppendNewData"/>

        </StackLayout>

        <ListView
            x:Name="scheduleList">
... 省略 ...
        </ListView>
    </StackLayout>
</StackLayout>

コードビハインド側にはボタンタップ時に実行されるメソッドを追加します。

MainPage.xaml.cs
public void ChangeFirstTitle(object sender, EventArgs e)
{
    ScheduleListData[0].Title = "The Concert is Canceled!!!";
}

public void AppendNewData(object sender, EventArgs e)
{
    ScheduleListData.Add(new Schedule("2019/01/29", "Enjoy Onsen Ryokan in Chichibu", "Chichibu, Saitama"));
}

これで、ボタンの準備は完了です。

現状ではこのボタンを押してScheduleListDataの中身を追加・変更しても特にUIに変化はありません。

では、ここから動的なListViewのUI変更に対応していきます。

データの追加に対応する

まずデータがScheduleListに追加された場合に、追加された項目がリストへ自動で表示されるようにします。

対応内容自体は簡単で、データソースの型にList<T>ではなくObservableCollection<T>を使うだけです。

MainPage.xaml.cs
private ObservableCollection<Schedule> ScheduleListData = new ObservableCollection<Schedule>();

実行すると、以下のように"Append Data"ボタンを押すたびにリストの項目が1件ずつ追加されているのがわかるかと思います。

ObservableCollection<T>は、Microsoftのドキュメントによると

項目が追加または削除されたとき、あるいはリスト全体が更新されたときに通知を行う動的なデータ コレクションを表します。

とのことです。詳しい仕組みはまだ調査できていませんが、要するにObservableCollection<T>ListViewのデータソースとして利用することで、ObservableCollection<T>が発する通知をListViewが受信し、UIを更新してくれるような作りになっているのだと思います。

データの変更に対応する

ObservableCollection<T>はリスト自体の状態の変化は検知できても、リストに格納される一つひとつのインスタンスの状態の変化は検知しません。そのため、個別のデータのプロパティが変化してもUIには反映されません。

個別のデータのプロパティの変化を検知してListViewがUIを動的に変更できるようにするために、INotifyPropertyChangedインターフェースを利用します。

先ほどのObservableCollection<T>がやっていた変更通知を自分のデータクラスにも実装してあげるイメージですね。この方法はListViewに限らずC#においてModelがプロパティの値の変更を通知する手段としてよく利用されるもののようです(調査中)。

というわけで、Scheduleクラスを以下のように修正します。

  • INotifyPropertyChangedを実装する
  • public event PropertyChangedEventHandler PropertyChangedを定義する
  • セッターでPropertyChanged.Invoke()を呼ぶ

上記を対応したコードが以下になります。

MainPage.xaml.cs
    public class Schedule : INotifyPropertyChanged
    {
private string datestr;
        public string Datestr
        {
            get => this.datestr;
            set
            {
                this.datestr = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Datestr)));
            }
        }

        private string title;
        public string Title
        {
            get => this.title;
            set
            {
                this.title = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Title)));
            }
        }

        private string place;
        public string Place
        {
            get => this.place;
            set
            {
                this.place = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Place)));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }

動作確認をすると、"Change Title"ボタンを押すと1件目の予定のタイトルが"The Concert is Canceled!!!"に変化しているのが分かります。

これで、個別のデータのプロパティが変化した場合も、動的にUIが変化するようになりました。

まとめ

AndroidやiOSのネイティブアプリを経験していると、notifyDatasetChanged()reloadData()のようなメソッドがListViewにあるのではないかという気がしてきますが、見た感じXamarin.FormsのListViewにはそのようなメソッドはありませんでした。

Xamarin.Formsでは、リストのUIは自分で更新するものではなく、データバインディングの仕組みを利用してデータ側からListViewへ通知を送り、UIが自動で更新されるものと考えるのがよさそうです。

この記事のコード

コードはGitHubで公開しています。

chooyan-eng/XamarinPractice

今回の記事の分のコミットは以下のあたりです。


  1. ContentPageは子ビューを1つだけとるレイアウトですので、ListViewとボタンを並べるStackLayoutをさらにStackLayoutでひとまとめにする必要がありますので注意です。