📜

Virtualize された MudDataGrid で ScrollTo を行う (MudBlazor)

に公開

はじめに

この記事は、以下を下敷きにしています。

https://zenn.dev/tetr4lab/articles/6b1a70ca7800ed

MudBlazorについても、そちらをご参照ください。
また、以降で挙げるコードの詳細は、次のリポジトリをご参照ください。

アプリケーション

https://github.com/tetr4lab/Novels

ライブラリパッケージ

https://github.com/tetr4lab/Tetr4labNugetPackages

やりたいこと

仮想化したMudDataGridで、任意の項目へのスクロールを実現したいです。

MudBlazorのスクロールマネージャには、CSSセレクタでターゲットを指定するメソッド(ScrollIntoViewAsync)があるので、例えば、グリッドの項目中に以下のようにアンカーを打っておくと、

    <a id="@($"{typeof (Book).Name}-{context.Item.Id}")" />

以下のコードで、項目が見えるところまでスクロールすることができます。

    [Inject] protected IScrollManager ScrollManager { get; set; } = null!;
//~
        await ScrollManager.ScrollIntoViewAsync ($"a#{typeof (T).Name}-{focusedId}", ScrollBehavior.Auto);

しかし、MudDataGridVirtualize="true"にしてHeight="~"でサイズを指定して使うと、見えていない項目がレンダリングされなくなる(=仮想化される)ため、上記の方法ではスクロールできなくなります。

蛇足ですが…

いったんレンダリングされた項目が隠れた場合、通常は、オブジェクトが抹消され、見えている範囲+αだけがインスタンス化されるようです。
しかし、グルーピングされているグリッドでは、いったんレンダリングされた項目は抹消が遅くなるようで、表示範囲から遠い項目でも残っていることがあります。
そのため、前述のScrollIntoViewAsyncでスクロールできたり、できなかったりして、紛らわしいです。

また、MudDataGridには、ページネーションを前提にしたNavigateToというメソッドがあるのですが、仮想化を前提にしたScrollToはないようです。

やったこと

スクロールマネージャに、スクロール量を指定するメソッド(ScrollToAsync)があるので、そちらを利用しました。
下記のうち、まず「実装 B」を、次に「実装 A」を試した後、最終的に「B」を採用しました。

なお、このアプリでは、以下のようにプリレンダリングを行っていません。

https://github.com/tetr4lab/Novels/blob/e04222b8f29474d4aa19deb2bb2fd58d9b1aa3b3/Novels/Novels/Components/App.razor#L20

実装 A

グリッドや項目のサイズを動的に取得してスクロール量をまじめに算定する方式です。

グリッドの実装

https://github.com/tetr4lab/Novels/blob/effcbbb1a966af2883b7d96ad5df10045c43d622/Novels/Novels/Components/Pages/Books.razor#L11-L15

  • Heightは、ビューポートの高さから上下AppBarのサイズとマージン(1em)を除いた残りです。
    • マージンは、実際に表示して調整しています。(ブラウザ依存の可能性)

https://github.com/tetr4lab/Novels/blob/effcbbb1a966af2883b7d96ad5df10045c43d622/Novels/Novels/Components/Pages/Books.razor#L53-L55

  • グリッドの項目が全て同じ高さになるように、一定の幅を超えた部分は非表示にしています。

サイズを取得

以下のJS相互運用で、DOM要素の位置とサイズを取得します。

https://github.com/tetr4lab/Tetr4labNugetPackages/blob/2ba41006b3bd7084a2a907b421d638b0428b7191/Tetr4labRazor/wwwroot/js/Tetr4labUtilities.js#L18-L34

https://github.com/tetr4lab/Tetr4labNugetPackages/blob/2ba41006b3bd7084a2a907b421d638b0428b7191/Tetr4labRazor/JSRuntimeHelper.cs#L6-L23

https://github.com/tetr4lab/Tetr4labNugetPackages/blob/2ba41006b3bd7084a2a907b421d638b0428b7191/Tetr4labRazor/JSRuntimeHelper.cs#L73-L76

スクロール量を算定

取得したサイズを元に、スクロール量を「行高 * index - テーブル高の半分」で算定します。
テーブルの中央に対象項目が来るように、高さの半分を減じています。

https://github.com/tetr4lab/Tetr4labNugetPackages/blob/2ba41006b3bd7084a2a907b421d638b0428b7191/Tetr4labRazor/ScrollManagerHelper.cs#L19-L37

インデックスの算定とスクロール

使用箇所では、絞り込みに配慮して、対象の項目のインデックスを算定して渡します。

https://github.com/tetr4lab/Novels/blob/effcbbb1a966af2883b7d96ad5df10045c43d622/Novels/Novels/Components/Pages/ItemListBase.cs#L181-L196

実際にやってみると、最初のレンダリング直後(OnAfterRenderAsyncfirstRender == true)は、まだグリッドのレンダリングが終わっていない部分があったり、再計算が行われて再レンダリングされたりするようなので、サイズが安定するまで待つようになっています。

結果、妙にモッサリしたり、速度を優先しようとすると不安定になったりします。

本来は、Task.Delay ()でなく、Blazorのレンダリングサイクルに同期して待つようにすべきなのでしょうが、煩雑になるのでやっていません。

実装 B

あらかじめ実測したグリッドや項目のサイズを定数として埋め込む方式です。
実は、当初、こちらで実装しましたが、柔軟性に配慮して、動的なサイズの取得(実装 A)を試行しました。
試行錯誤の末、最終的にこちらを採用しています。

グリッドの実装とサイズ情報

Breakpoint="Breakpoint.None"にしてレイアウトを固定しています。

https://github.com/tetr4lab/Novels/blob/e04222b8f29474d4aa19deb2bb2fd58d9b1aa3b3/Novels/Novels/Components/Pages/Books.razor#L11-L15

ItemSizeVirtulize用とされていますが、何を設定しても、しなくても、変化が見られず、どう使われるのかよく分かりません。
一般化に都合が良かったので使用しています。

https://github.com/tetr4lab/Novels/blob/e04222b8f29474d4aa19deb2bb2fd58d9b1aa3b3/Novels/Novels/Components/Pages/Books.razor#L144-L148

https://github.com/tetr4lab/Novels/blob/e04222b8f29474d4aa19deb2bb2fd58d9b1aa3b3/Novels/Novels/Components/Pages/Sheets.razor#L112-L116

ViewportHeightRatioは、ビューポートの高さに対してグリッドの占める比率っぽい係数です。

諸々の算定とスクロール

ビューポートサイズだけは動的に取得することで、ウインドウサイズの変化に追従するようになっています。

https://github.com/tetr4lab/Novels/blob/e04222b8f29474d4aa19deb2bb2fd58d9b1aa3b3/Novels/Novels/Components/Pages/ItemListBase.cs#L28

https://github.com/tetr4lab/Novels/blob/e04222b8f29474d4aa19deb2bb2fd58d9b1aa3b3/Novels/Novels/Components/Pages/ItemListBase.cs#L181-L203

おわりに

お読みいただきありがとうございました。
執筆者は、Blazor、ASP.NETなど諸々において初学者ですので、誤りもあるかと思います。
お気づきの際は、是非コメントや編集リクエストにてご指摘ください。
あるいは、「それでも解らない」、「自分はこう捉えている」などといった、ご意見、ご感想も歓迎いたします。

Discussion