0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Modelが取得した最大n件のデータを固定のn件にしてViewにバインドする

Last updated at Posted at 2021-09-21

不定件数のデータを、固定の件数で画面表示

最近はスマホやタブレットでも十分な性能がありますが、とはいえ画面サイズは小さいのでサーバーの問い合わせ結果をすべて一度に画面表示することはできません。あるいは、巨大なモニタに一定量のデータを並べて公衆に向けて表示させるような場合、モニタしかないので大衆がスクロールをすることはできません。
そこで、データ数によらず画面項目の表示位置を固定化させたいようなケースでは以下のような仕様にすることがよくあります。

  1. 画面には1ページあたり最大n件のみ表示する
    1. 例えばn=8の場合、8このカードを必ず画面に表示するイメージ
  2. サーバーへの問い合わせ結果のデータ件数は0件以上n件以下になる
    1. 画面には8件しか出ないので毎回全件取得するのは無駄なため抽出件数を絞る
  3. データ全体の数がnで割り切れない場合、あまり部分は空白にせず、ダミーの空データを表示する

具体的な例としてはバスや空港の発着時刻の表示とか、機械の稼働状況を監視する画面を作る場合ですね。
この場合、ReactiveExtensionsを活用すると以下のようなコーディングになるでしょう。

  1. INotifyPropertyChangedを実装するModelで不定件数のデータ(ただし高々n件)を取得してpublicプロパティにセット
  2. ViewModelでModelをObserveProperty(プロパティ)してIObservable<T>にした後、n件に加工してReadOnlyReactiveなプロパティにする
  3. ViewのxamlでViewModelのReadOnlyReactiveなプロパティにバインド

複数データのバインドをするということで、ModelのプロパティをObservableCollectionにしてToReadOnlyReactiveCollectionするのがすぐ思いつきます。しかし、こいつらを使ってもうまくいきません。(前略)ナンデ!?

ObservableCollectionを使えない理由

抽出件数を絞る以上、Modelの取得結果に前回取得した値や画面に表示しない値を保持することは無意味です1。そのため、画面の更新のたびに毎回ObservableCollectionClearしてAddRangeする必要があります。ClearAddRangeをするとそのたびに変更通知が走ってしまいます。
また、ToReadOnlyReactiveCollectionではAddRangeするたびにデータが減ることなくどんどん溜まっていってしまいます。そのため、同じキーのデータなのにコレクションの中には複数のデータ(古い値と新しい値)が登録されてしまいます。

じゃあどうやるの

  1. データ更新のたびに全件洗い替えになる
  2. 件数は固定

ということであれば、配列でいいです

  • ModelはIEnumerable<T>でデータを保持し、抽出のたびに全体を作り直す
  • ViewModelはReactiveProperty<データ[]>のプロパティで保持する
    • 画面からの更新がないならReadOnlyReactivePropertyを使う
  • Viewは表示項目それぞれにBinding={"VMのプロパティ.Value[index]"}でバインドする

ポイントは、ModelはIEnumerable<T>で持つということです。Modelの役割はあくまで「抽出処理の実行と結果の公開」であり、「固定でn件で表示する」というのは画面側、つまりViewModelの責任になります。生の結果で持っておけば件数や表示のさせ方を変えた別のViewModelでも使用できますよね。

以下にサンプルコードを示します。なおMVVMフレームワークにPrismを使用しています。

Model.cs
public class MyData { /* なんか任意のデータ */ }
public class Model : BindableBase // Prismの機能でINotifyPropertyChangedを実装
{
    // Request実行結果のコレクションは以下に格納し、変更時に通知する
    public IEnumerable<MyModel> Response { get => _res; set => SetProperty(ref _res, value); }
    private IEnumerable<MyModel> _res = Array.Empty<MyModel>();

    public void Request() { /* なんか取得処理 */ }
}
ViewModel.cs
public class ViewModel
{
    private Model _model;
    public ReadOnlyReactiveProperty<MyData[]> Prop { get; }
    public const int DisplayCount = 8; // 最大8件
    private static MyData Empty { get; } = ~~; // ダミーデータ
    public ViewModel(Model model)
    {
        _model = model;
        Prop = _model.
            // IObservable<IEnumerable<Mydata>>の内容を監視して
            ObserveProperty(m => m.Response).
            // 固定で8件(足りなければEmptyをつける)のIObservable<MyData[]>に変換して
            Select(d => (d.Count() == DisplayCount ? d :
                         d.Concat(Enumerable.Repeat(Empty, DisplayCount - d.Count()))).ToArray()).
            // 配列型のReadOnlyReactivePropertyにする
            // @note : null参照許容型(.csprojの<Nullable>オプション)をenableにしている場合、
            // 型を明示しないとReadOnlyReactiveProperty<MyData[]?>に推論されて警告される
            ToReadOnlyReactiveProperty<MyData[]>();
    }
}
View.xaml
<!-- 4行2列とする -->
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="320*"/>
            <ColumnDefinition Width="320*"/>
            <ColumnDefinition Width="320*"/>
            <ColumnDefinition Width="320*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
<!-- .ValueでReactivePropertyの中身を取れて、 -->
<!-- .Valueが配列ということは、 -->
<!-- .Value[index] で各要素をとれる -->
        <UserControls:MyParts Grid.Row="0" Grid.Column="0" DataContext="{Binding Prop.Value[0]}"/>
        <UserControls:MyParts Grid.Row="0" Grid.Column="1" DataContext="{Binding Prop.Value[1]}"/>
        <UserControls:MyParts Grid.Row="0" Grid.Column="2" DataContext="{Binding Prop.Value[2]}"/>
        <UserControls:MyParts Grid.Row="0" Grid.Column="3" DataContext="{Binding Prop.Value[3]}"/>
        <UserControls:MyParts Grid.Row="1" Grid.Column="0" DataContext="{Binding Prop.Value[4]}"/>
        <UserControls:MyParts Grid.Row="1" Grid.Column="1" DataContext="{Binding Prop.Value[5]}"/>
        <UserControls:MyParts Grid.Row="1" Grid.Column="2" DataContext="{Binding Prop.Value[6]}"/>
        <UserControls:MyParts Grid.Row="1" Grid.Column="3" DataContext="{Binding Prop.Value[7]}"/>
    </Grid>

補足

上記の例では説明を省略してしまいましたが、「固定長の文字列配列を表示」ということはあまり行わないと思います。そのため本稿は「ユーザーコントロールに独自の型の配列をバインドする」ということを行っています。
ユーザーコントロールに独自型をバインドするには以下のどちらかの方法を採る必要があります(本稿は後者)。

  • 依存関係プロパティを作る
  • DataContextを設定する

これについてはこちらの記事:WPFのユーザーコントロールは依存関係プロパティを作らなくても独自型をバインドできるという話を参照してください。

  1. 画面に表示しない値を保持しても、その項目を表示したくなったタイミングで値が変わっている可能性があるため、結局サーバーに再取得しにいくことになります。勿論要件によります

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?