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

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

[【Xamarin.Forms】ListView の CustomCell を XAML で定義する](https://qiita.com/chooyan_eng/items/4f1b037113a8e72edf90)

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

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

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

[ListViewで学ぶコレクションのBind通知 - @furugen](https://qiita.com/furugen/items/18bd2a521d1fa9927212)

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

# 本文

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

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

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

```xml: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>
```

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

```c#: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に変化はありません。

![qiita_listview_1.gif](https://qiita-image-store.s3.amazonaws.com/0/134943/8e04eff5-8c34-161e-3c3b-a8f7de948ba3.gif)

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

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

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

```c#:MainPage.xaml.cs
private ObservableCollection<Schedule> ScheduleListData = new ObservableCollection<Schedule>();
```

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

![qiita_listview_2.gif](https://qiita-image-store.s3.amazonaws.com/0/134943/d0735448-5428-ab15-5ec7-d198698c32e6.gif)

`ObservableCollection<T>`は、[Microsoftのドキュメント](https://docs.microsoft.com/ja-jp/dotnet/api/system.collections.objectmodel.observablecollection-1?view=netframework-4.7.2)によると

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

とのことです。詳しい仕組みはまだ調査できていませんが、要するに`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()`を呼ぶ

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

```c#: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"ボタンを押すと１件目の予定のタイトルが"The Concert is Canceled!!!"に変化しているのが分かります。

![qiita_listview_3.gif](https://qiita-image-store.s3.amazonaws.com/0/134943/a4d28867-150e-47e1-1cf0-06bb5c746725.gif)

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

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

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


# この記事のコード
コードはGitHubで公開しています。

[chooyan-eng/XamarinPractice](https://github.com/chooyan-eng/XamarinPractice)

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

https://github.com/chooyan-eng/XamarinPractice/commit/8ddf54b06af2edfa7cc7391b5f3a9797411255f0

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