不定件数のデータを、固定の件数で画面表示
最近はスマホやタブレットでも十分な性能がありますが、とはいえ画面サイズは小さいのでサーバーの問い合わせ結果をすべて一度に画面表示することはできません。あるいは、巨大なモニタに一定量のデータを並べて公衆に向けて表示させるような場合、モニタしかないので大衆がスクロールをすることはできません。
そこで、データ数によらず画面項目の表示位置を固定化させたいようなケースでは以下のような仕様にすることがよくあります。
- 画面には1ページあたり最大
n
件のみ表示する- 例えば
n=8
の場合、8このカードを必ず画面に表示するイメージ
- 例えば
- サーバーへの問い合わせ結果のデータ件数は
0
件以上n
件以下になる- 画面には8件しか出ないので毎回全件取得するのは無駄なため抽出件数を絞る
- データ全体の数が
n
で割り切れない場合、あまり部分は空白にせず、ダミーの空データを表示する
具体的な例としてはバスや空港の発着時刻の表示とか、機械の稼働状況を監視する画面を作る場合ですね。
この場合、ReactiveExtensions
を活用すると以下のようなコーディングになるでしょう。
-
INotifyPropertyChanged
を実装するModelで不定件数のデータ(ただし高々n
件)を取得してpublic
プロパティにセット - ViewModelでModelを
ObserveProperty(プロパティ)
してIObservable<T>
にした後、n
件に加工してReadOnlyReactive
なプロパティにする - ViewのxamlでViewModelの
ReadOnlyReactive
なプロパティにバインド
複数データのバインドをするということで、ModelのプロパティをObservableCollection
にしてToReadOnlyReactiveCollection
するのがすぐ思いつきます。しかし、こいつらを使ってもうまくいきません。(前略)ナンデ!?
ObservableCollectionを使えない理由
抽出件数を絞る以上、Modelの取得結果に前回取得した値や画面に表示しない値を保持することは無意味です1。そのため、画面の更新のたびに毎回ObservableCollection
をClear
してAddRange
する必要があります。Clear
やAddRange
をするとそのたびに変更通知が走ってしまいます。
また、ToReadOnlyReactiveCollection
ではAddRange
するたびにデータが減ることなくどんどん溜まっていってしまいます。そのため、同じキーのデータなのにコレクションの中には複数のデータ(古い値と新しい値)が登録されてしまいます。
じゃあどうやるの
- データ更新のたびに全件洗い替えになる
- 件数は固定
ということであれば、配列でいいです。
- Modelは
IEnumerable<T>
でデータを保持し、抽出のたびに全体を作り直す - ViewModelは
ReactiveProperty<データ[]>
のプロパティで保持する- 画面からの更新がないなら
ReadOnlyReactiveProperty
を使う
- 画面からの更新がないなら
- Viewは表示項目それぞれに
Binding={"VMのプロパティ.Value[index]"}
でバインドする
ポイントは、ModelはIEnumerable<T>
で持つということです。Modelの役割はあくまで「抽出処理の実行と結果の公開」であり、「固定でn
件で表示する」というのは画面側、つまりViewModelの責任になります。生の結果で持っておけば件数や表示のさせ方を変えた別のViewModelでも使用できますよね。
以下にサンプルコードを示します。なおMVVMフレームワークにPrismを使用しています。
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() { /* なんか取得処理 */ }
}
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[]>();
}
}
<!-- 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のユーザーコントロールは依存関係プロパティを作らなくても独自型をバインドできるという話を参照してください。
-
画面に表示しない値を保持しても、その項目を表示したくなったタイミングで値が変わっている可能性があるため、結局サーバーに再取得しにいくことになります。勿論要件によります ↩