Razor Pages で仮想スクロールを実装する

オルダー
質問: 行数の多いテーブルの表示
テーブルタグで表を表示しているのですが、項目が多く行数が増えページがとても長くなります。 長くなるだけならよいのですが、表示やスクロールがかなり重くなってしまいます。とはいえ、ページ繰りのUIにするのも閲覧性が下がりよくないと考えています。 何か良い対処方法はありますか?

仮想スクロール

仮想スクロールを実装することで改善できます。仮想スクロールとは、見た目は要素を全部表示しますが、DOMは必要な分だけ描画する方式です。

仮想スクロールの挙動

  • スクロールに応じて 表示領域+α だけを描画
  • 画面外の行はDOMから外す(でも高さは保持)
  • ユーザー体験は「全部並んでる」ように見える

メリット

  • ページ繰り不要
  • スクロールの動作が速い

補足

  • 行の高さは 固定 or 概ね一定 が理想、行の高さがまちまちの場合、スクロールバーの処理がうまくできない
  • 画面サイズで見える範囲(例:120〜160件)の要素を表示すると使用感が良いです
  • 行に折り返しが多い場合、終端までスクロールした際に、ちらつき、ガタつきが発生する場合があります
注意
ドラッグして内部のコンテンツを選択してコピーしても、画面上で見えている部分しかコピーされない点も注意が必要です。 また、ページ内を検索した場合も見える部分での文字列しか検索にヒットしません。

実装

ASP.NET Coreアプリケーションを作成します。ファイルは以下です。

メモ
今回の記事では、RazorPagesでの実装を紹介していますが、画面側はJavaScriptで実装するため、JSONを出力できる仕組みが別途実装できれば、 画面側はRazorPagesでなくても良いです。(HTML+JavaScriptでよい)

Razor Pages で仮想スクロールを実装する:画像1

コード

以下のコードを記述します。

Program.cs
namespace RazorPagesVirtualScroll
{
  public class Program
  {
    public static void Main(string[] args)
    {
      var builder = WebApplication.CreateBuilder(args);
      builder.Services.AddRazorPages();
      var app = builder.Build();

      if (app.Environment.IsDevelopment()) {
        app.UseDeveloperExceptionPage();
      }

      app.UseStaticFiles();
      app.UseRouting();
      app.MapRazorPages();

      //JSONを出力するAPIエンドポイントを追加
      app.MapGet("/api/items", () => Results.Json(DemoData.CreateItems()));

      app.Run();
    }
  }
}

Index.cshtml
@page
@model RazorPagesVirtualScroll.Pages.IndexModel
@{
}

<!-- スタイルシートの読み込み -->
<link rel="stylesheet" href="~/index.css" />

<h1>仮想スクロール デモ</h1>

<!-- テーブルヘッダー行 -->
<div class="header-row">
    <span class="col-id">ID</span>
    <span class="col-category">カテゴリ</span>
    <span class="col-name">商品名</span>
    <span class="col-price">価格</span>
    <span class="col-stock">在庫</span>
</div>

<!-- 仮想スクロールコンテナ -->
<!-- spacer: 全アイテム分の高さを確保してスクロールバーを生成する -->
<!-- viewport: 表示範囲のアイテムだけを描画する領域 -->
<div id="virtual-scroll-container" class="virtual-scroll-container">
    <div id="spacer" class="virtual-scroll-spacer"></div>
    <div id="viewport" class="virtual-scroll-viewport"></div>
</div>

<script>
    (function () {
        // 各アイテム行の高さ (px)
        const ITEM_HEIGHT = 40;
        // 表示範囲の上下に追加描画するバッファ行数
        const BUFFER = 5;

        // DOM 要素の取得
        const container = document.getElementById('virtual-scroll-container');
        const spacer = document.getElementById('spacer');
        const viewport = document.getElementById('viewport');

        // API から取得した全アイテムを保持する配列
        let allItems = [];

        // API からアイテムデータを取得する
        async function loadItems() {
            const response = await fetch('/api/items');
            allItems = await response.json();
            // spacer の高さを全アイテムの合計高さに設定し、スクロールバーを生成する
            spacer.style.height = (allItems.length * ITEM_HEIGHT) + 'px';
            render();
        }

        // 現在のスクロール位置に応じて表示範囲のアイテムだけを描画する
        function render() {
            const scrollTop = container.scrollTop;
            const containerHeight = container.clientHeight;

            // 表示開始インデックスを計算 (バッファ分を上に拡張)
            let startIndex = Math.floor(scrollTop / ITEM_HEIGHT) - BUFFER;
            if (startIndex < 0) startIndex = 0;

            // 表示終了インデックスを計算 (バッファ分を下に拡張)
            let endIndex = Math.ceil((scrollTop + containerHeight) / ITEM_HEIGHT) + BUFFER;
            if (endIndex > allItems.length) endIndex = allItems.length;

            // viewport の位置を開始インデックスに合わせてオフセットする
            viewport.style.top = (startIndex * ITEM_HEIGHT) + 'px';

            // 表示範囲のアイテムの HTML を組み立てる
            let html = '';
            for (let i = startIndex; i < endIndex; i++) {
                const item = allItems[i];
                html += '<div class="item-row">'
                    + '<span class="col-id">' + item.id + '</span>'
                    + '<span class="col-category">' + item.category + '</span>'
                    + '<span class="col-name">' + item.product_name + '</span>'
                    + '<span class="col-price">&yen;' + item.price.toLocaleString() + '</span>'
                    + '<span class="col-stock">' + item.stock + '</span>'
                    + '</div>';
            }
            // viewport に HTML を反映する
            viewport.innerHTML = html;
        }

        // スクロールイベントで再描画を実行する
        container.addEventListener('scroll', render);
        // 初期データの読み込み
        loadItems();
    })();
</script>

ListItem.cs
namespace RazorPagesVirtualScroll
{
    public class ListItem
    {
        public int id { get; set; }
        public string category { get; set; } = "";
        public string product_name { get; set; } = "";
        public int price { get; set; }
        public int stock { get; set; }
    }
}

DemoData.cs
namespace RazorPagesVirtualScroll
{
    public static class DemoData
    {
        public static List<ListItem> CreateItems()
        {
            var categories = new[] { "果物", "野菜", "飲料", "菓子", "乳製品", "肉類", "魚介類", "調味料", "冷凍食品", "パン" };

            var products = new Dictionary<string, string[]>
            {
                ["果物"]   = ["りんご", "みかん", "バナナ", "ぶどう", "いちご", "桃", "梨", "柿", "メロン", "スイカ"],
                ["野菜"]   = ["トマト", "きゅうり", "にんじん", "じゃがいも", "たまねぎ", "キャベツ", "ほうれん草", "ピーマン", "なす", "大根"],
                ["飲料"]   = ["緑茶", "コーヒー", "オレンジジュース", "牛乳", "炭酸水", "紅茶", "スポーツドリンク", "ミネラルウォーター", "りんごジュース", "ココア"],
                ["菓子"]   = ["チョコレート", "クッキー", "ポテトチップス", "キャンディ", "せんべい", "ケーキ", "プリン", "ゼリー", "ドーナツ", "アイスクリーム"],
                ["乳製品"] = ["ヨーグルト", "チーズ", "バター", "生クリーム", "スキムミルク", "カッテージチーズ", "練乳", "クリームチーズ", "サワークリーム", "モッツァレラ"],
                ["肉類"]   = ["鶏もも肉", "豚バラ肉", "牛ロース", "合い挽き肉", "鶏むね肉", "豚ロース", "牛バラ肉", "ラム肉", "ソーセージ", "ベーコン"],
                ["魚介類"] = ["サーモン", "マグロ", "エビ", "イカ", "タコ", "サバ", "アジ", "ホタテ", "カニ", "ブリ"],
                ["調味料"] = ["醤油", "味噌", "塩", "砂糖", "酢", "みりん", "料理酒", "ケチャップ", "マヨネーズ", "ソース"],
                ["冷凍食品"] = ["冷凍餃子", "冷凍ピザ", "冷凍うどん", "冷凍チャーハン", "冷凍コロッケ", "冷凍枝豆", "冷凍たこ焼き", "冷凍パスタ", "冷凍ハンバーグ", "冷凍シュウマイ"],
                ["パン"]   = ["食パン", "クロワッサン", "メロンパン", "カレーパン", "フランスパン", "あんパン", "ベーグル", "ロールパン", "デニッシュ", "蒸しパン"],
            };

            var random = new Random(42);
            var items = new List<ListItem>();
            int id = 1;

            foreach (var category in categories)
            {
                foreach (var product in products[category])
                {
                    items.Add(new ListItem
                    {
                        id = id++,
                        category = category,
                        product_name = product,
                        price = random.Next(50, 3000),
                        stock = random.Next(0, 200)
                    });
                }
            }

            return items;
        }
    }
}

wwwroot/index.css
.virtual-scroll-container {
    height: 600px;
    overflow-y: auto;
    border: 1px solid #ccc;
    position: relative;
    width: 800px;
}

.virtual-scroll-spacer {
    width: 100%;
}

.virtual-scroll-viewport {
    position: absolute;
    left: 0;
    right: 0;
}

.item-row {
    display: flex;
    align-items: center;
    height: 40px;
    box-sizing: border-box;
    border-bottom: 1px solid #eee;
    padding: 0 12px;
    font-size: 14px;
}

.item-row:hover {
    background-color: #f0f8ff;
}

.item-row .col-id         { width:  60px; }
.item-row .col-category   { width: 100px; }
.item-row .col-name       { width: 260px; }
.item-row .col-price      { width: 100px; text-align: right; }
.item-row .col-stock      { width: 100px; text-align: right; }

.header-row {
    display: flex;
    align-items: center;
    height: 40px;
    padding: 0 12px;
    font-weight: bold;
    background-color: #4a90d9;
    color: #fff;
    font-size: 14px;
    width: 800px;
    box-sizing: border-box;
}

.header-row .col-id       { width:  60px; }
.header-row .col-category { width: 100px; }
.header-row .col-name     { width: 260px; }
.header-row .col-price    { width: 100px; text-align: right; }
.header-row .col-stock    { width: 100px; text-align: right; }

解説

Program.cs

以下のコードでJSONを返すAPIを定義しています。大量の項目をJSONで受け取り、そのうち必要な部分のみを画面に表示する動作です。

app.MapGet("/api/items", () => Results.Json(DemoData.CreateItems()));

DemoData.cs

JSONは、DemoData.cs の CreateItems() メソッドで作成されます。

JSONの出力は以下の形式です。

[{"id":1,"category":"果物","product_name":"りんご","price":2020,"stock":28},{"id":2,"category":"果物","product_name":"みかん","price":420,"stock":104},{"id":3,"category":"果物","product_name":"バナナ","price":546,"stock":52},{"id":4,"category":"果物","product_name":"ぶどう","price":2187,"stock":102},{"id":5,"category":"果物","product_name":"いちご","price":562,"stock":152},{"id":6,"category":"果物","product_name":"桃","price":742,"stock":51},
......]

Index.cshtml

JSONはページ表示時にJavaScriptでリクエストされます。

/apt/items のURLにアクセスしてJSONデータを取得します。

  // API からアイテムデータを取得する
  async function loadItems() {
    const response = await fetch('/api/items');
    allItems = await response.json();
    // spacer の高さを全アイテムの合計高さに設定し、スクロールバーを生成する
    spacer.style.height = (allItems.length * ITEM_HEIGHT) + 'px';
    render();
  }


スクロール時にrender関数が呼び出されます。
スクロール位置に対して見えるコンテンツのHTMLだけをビューポートに描画しています。

行の高さは40pxと想定し、表示範囲の上下のバッファ領域は5行としています。
スクロール量を行の高さで割り、バッファ分を差し引く、または、加算して、表示する範囲の最初の行と最後の行のインデックスを求めます。
ビューポートのtopの値に開始インデックスに行の高さを掛けた値を設定し、開始インデックスから画面に描画します。
HTMLを整形して画面に表示しています。今回の例ではTableタグを利用するのではなく、span タグを利用して表組を作成しています。

  // 各アイテム行の高さ (px)
  const ITEM_HEIGHT = 40;
  // 表示範囲の上下に追加描画するバッファ行数
  const BUFFER = 5;


  // 現在のスクロール位置に応じて表示範囲のアイテムだけを描画する
  function render() {
    const scrollTop = container.scrollTop;
    const containerHeight = container.clientHeight;

    // 表示開始インデックスを計算 (バッファ分を上に拡張)
    let startIndex = Math.floor(scrollTop / ITEM_HEIGHT) - BUFFER;
    if (startIndex < 0) startIndex = 0;

    // 表示終了インデックスを計算 (バッファ分を下に拡張)
    let endIndex = Math.ceil((scrollTop + containerHeight) / ITEM_HEIGHT) + BUFFER;
    if (endIndex > allItems.length) endIndex = allItems.length;

    // viewport の位置を開始インデックスに合わせてオフセットする
    viewport.style.top = (startIndex * ITEM_HEIGHT) + 'px';

    // 表示範囲のアイテムの HTML を組み立てる
    let html = '';
    for (let i = startIndex; i < endIndex; i++) {
      const item = allItems[i];
      html += '<div class="item-row">'
        + '<span class="col-id">' + item.id + '</span>'
        + '<span class="col-category">' + item.category + '</span>'
        + '<span class="col-name">' + item.product_name + '</span>'
        + '<span class="col-price">&yen;' + item.price.toLocaleString() + '</span>'
        + '<span class="col-stock">' + item.stock + '</span>'
        + '</div>';
    }
    // viewport に HTML を反映する
    viewport.innerHTML = html;
  }

ページ表示時にloadItems()関数を呼び出し、JSONを取得します。
また、addEventListenerメソッドを呼び出し、枠がスクロールされたらrender関数を呼び出すイベントを追加しています。

  (function () {
    /* 中略 */

    // スクロールイベントで再描画を実行する
    container.addEventListener('scroll', render);
    // 初期データの読み込み
    loadItems();
  })();

実行結果

プロジェクトを実行し、アプリケーションルートのURLにアクセスします。下図のページが表示されます。
Razor Pages で仮想スクロールを実装する:画像2

枠にスクロールバーが表示され枠内がスクロールできます。
Razor Pages で仮想スクロールを実装する:画像3

一番下までスクロールした状態です。
Razor Pages で仮想スクロールを実装する:画像4

プロンプト

以下の指示でコードを生成できます。Claude Opus 4.6, Visual Studioのエージェントモードで実行しています。

Prompt1
RazorPages のシンプルな仮想スクロールのデモプログラムを作成したいです。 アイテムクラス ListItem.cs を作成します。 ListItem.cs内には
int id
string category
string product_name
int price
int stock
のメンバを持ちます。
デモデータとして、List<ListItem> を作成し、100個程度のデモデータを作成します。 このデモデータをJSON出力し、 RazorPages側でJavaScriptでアクセスして仮想スクロールのデモを実装したいです。
Prompt2
index.cshtml にあるスタイルシートの定義を wwwroot/index.css に移動してください。
デモプログラムなので、DemoData.cs ListItem.cs をサブディレクトリに配置しないほうが良いです。Program.cs と同じディレクトリに移動してください

Tableタグでの実装版

表組をTableタグで実装した場合の例です。

Index.cshtml
@page
@model RazorPagesVirtualScroll.Pages.IndexTableTagModel
@{
}

<!-- スタイルシートの読み込み -->
<link rel="stylesheet" href="~/index-table-tag.css" />

<h1>仮想スクロール デモ (テーブルタグ版)</h1>

<!-- 仮想スクロールコンテナ -->
<div id="virtual-scroll-table-container" class="virtual-scroll-table-container">
    <table>
        <thead>
            <tr>
                <th class="col-id">ID</th>
                <th class="col-category">カテゴリ</th>
                <th class="col-name">商品名</th>
                <th class="col-price">価格</th>
                <th class="col-stock">在庫</th>
            </tr>
        </thead>
        <tbody id="table-body">
            <!-- 上部スペーサー行: 表示範囲より上の行の高さを確保する -->
            <tr id="spacer-top"><td colspan="5"></td></tr>
            <!-- 表示範囲の行はスクリプトで挿入される -->
            <!-- 下部スペーサー行: 表示範囲より下の行の高さを確保する -->
            <tr id="spacer-bottom"><td colspan="5"></td></tr>
        </tbody>
    </table>
</div>

<script>
    (function () {
        // 各アイテム行の高さ (px)
        const ITEM_HEIGHT = 40;
        // 表示範囲の上下に追加描画するバッファ行数
        const BUFFER = 5;

        // DOM 要素の取得
        const container = document.getElementById('virtual-scroll-table-container');
        const tableBody = document.getElementById('table-body');
        const spacerTop = document.getElementById('spacer-top');
        const spacerBottom = document.getElementById('spacer-bottom');

        // API から取得した全アイテムを保持する配列
        let allItems = [];

        // API からアイテムデータを取得する
        async function loadItems() {
            const response = await fetch('/api/items');
            allItems = await response.json();
            render();
        }

        // 現在のスクロール位置に応じて表示範囲のアイテムだけを描画する
        function render() {
            const scrollTop = container.scrollTop;
            const containerHeight = container.clientHeight;

            // 表示開始インデックスを計算 (バッファ分を上に拡張)
            let startIndex = Math.floor(scrollTop / ITEM_HEIGHT) - BUFFER;
            if (startIndex < 0) startIndex = 0;

            // 表示終了インデックスを計算 (バッファ分を下に拡張)
            let endIndex = Math.ceil((scrollTop + containerHeight) / ITEM_HEIGHT) + BUFFER;
            if (endIndex > allItems.length) endIndex = allItems.length;

            // 上部スペーサーの高さを設定 (表示範囲より上の行分)
            spacerTop.style.height = (startIndex * ITEM_HEIGHT) + 'px';
            // 下部スペーサーの高さを設定 (表示範囲より下の行分)
            spacerBottom.style.height = ((allItems.length - endIndex) * ITEM_HEIGHT) + 'px';

            // 既存の表示行を削除する (スペーサー行は残す)
            const oldRows = tableBody.querySelectorAll('tr.item-row');
            for (const row of oldRows) {
                row.remove();
            }

            // 表示範囲のアイテム行を組み立てて挿入する
            const fragment = document.createDocumentFragment();
            for (let i = startIndex; i < endIndex; i++) {
                const item = allItems[i];
                const tr = document.createElement('tr');
                tr.className = 'item-row';
                tr.innerHTML =
                    '<td class="col-id">' + item.id + '</td>'
                    + '<td class="col-category">' + item.category + '</td>'
                    + '<td class="col-name">' + item.product_name + '</td>'
                    + '<td class="col-price">&yen;' + item.price.toLocaleString() + '</td>'
                    + '<td class="col-stock">' + item.stock + '</td>';
                fragment.appendChild(tr);
            }
            // スペーサー行の間にアイテム行を挿入する
            tableBody.insertBefore(fragment, spacerBottom);
        }

        // スクロールイベントで再描画を実行する
        container.addEventListener('scroll', render);
        // 初期データの読み込み
        loadItems();
    })();
</script>

wwwroot/index.css
.virtual-scroll-table-container {
    height: 600px;
    overflow-y: auto;
    border: 1px solid #ccc;
    width: 800px;
}

.virtual-scroll-table-container table {
    width: 100%;
    border-collapse: collapse;
    table-layout: fixed;
}

.virtual-scroll-table-container thead {
    position: sticky;
    top: 0;
    z-index: 1;
}

.virtual-scroll-table-container thead th {
    height: 40px;
    padding: 0 12px;
    font-weight: bold;
    background-color: #4a90d9;
    color: #fff;
    font-size: 14px;
    box-sizing: border-box;
    text-align: left;
}

.virtual-scroll-table-container thead th.col-price,
.virtual-scroll-table-container thead th.col-stock {
    text-align: right;
}

.virtual-scroll-table-container tbody td {
    height: 40px;
    padding: 0 12px;
    font-size: 14px;
    box-sizing: border-box;
    border-bottom: 1px solid #eee;
}

.virtual-scroll-table-container tbody td.col-price,
.virtual-scroll-table-container tbody td.col-stock {
    text-align: right;
}

.virtual-scroll-table-container tbody tr:hover td {
    background-color: #f0f8ff;
}

.virtual-scroll-table-container .col-id       { width:  60px; }
.virtual-scroll-table-container .col-category { width: 100px; }
.virtual-scroll-table-container .col-name     { width: 260px; }
.virtual-scroll-table-container .col-price    { width: 100px; }
.virtual-scroll-table-container .col-stock    { width: 100px; }


実行結果は下図です。

プロジェクトを実行し、アプリケーションルートのURLにアクセスします。下図のページが表示されます。
Razor Pages で仮想スクロールを実装する:画像5

枠にスクロールバーが表示され枠内がスクロールできます。
Razor Pages で仮想スクロールを実装する:画像6

一番下までスクロールした状態です。
Razor Pages で仮想スクロールを実装する:画像7

プロンプト

以下の指示でコードを生成できます。Claude Opus 4.6, Visual Studioのエージェントモードで実行しています。

Prompt1
index.cshtml ページではdivタグで表が組まれていますが、Tableタグで表組したいです。 IndexTableTag.cshtml ページにTableタグで表組された仮想スクロールのデモを実装できますか? JSONの取得は既存の物を利用して、極力修正しないでお願いしたいです。

ページモデルを利用した実装版

このセクションでは、RazorPagesのプロパティを利用してページモデルを利用して、ページのJavaScriptではFetchしない方式を紹介します。

IndexNoFetch.cshtml
@page
@model RazorPagesVirtualScroll.Pages.IndexNoFetchModel
@{
}

<!-- スタイルシートの読み込み -->
<link rel="stylesheet" href="~/index.css" />

<h1>仮想スクロール デモ (Fetch なし)</h1>

<!-- テーブルヘッダー行 -->
<div class="header-row">
    <span class="col-id">ID</span>
    <span class="col-category">カテゴリ</span>
    <span class="col-name">商品名</span>
    <span class="col-price">価格</span>
    <span class="col-stock">在庫</span>
</div>

<!-- 仮想スクロールコンテナ -->
<div id="virtual-scroll-container" class="virtual-scroll-container">
    <div id="spacer" class="virtual-scroll-spacer"></div>
    <div id="viewport" class="virtual-scroll-viewport"></div>
</div>

<script>
    (function () {
        // 各アイテム行の高さ (px)
        const ITEM_HEIGHT = 40;
        // 表示範囲の上下に追加描画するバッファ行数
        const BUFFER = 5;

        // DOM 要素の取得
        const container = document.getElementById('virtual-scroll-container');
        const spacer = document.getElementById('spacer');
        const viewport = document.getElementById('viewport');

        // ページモデルから埋め込まれた JSON データを取得する
        const allItems = @Html.Raw(Model.DataJson);

        // spacer の高さを全アイテムの合計高さに設定し、スクロールバーを生成する
        spacer.style.height = (allItems.length * ITEM_HEIGHT) + 'px';

        // 現在のスクロール位置に応じて表示範囲のアイテムだけを描画する
        function render() {
            const scrollTop = container.scrollTop;
            const containerHeight = container.clientHeight;

            // 表示開始インデックスを計算 (バッファ分を上に拡張)
            let startIndex = Math.floor(scrollTop / ITEM_HEIGHT) - BUFFER;
            if (startIndex < 0) startIndex = 0;

            // 表示終了インデックスを計算 (バッファ分を下に拡張)
            let endIndex = Math.ceil((scrollTop + containerHeight) / ITEM_HEIGHT) + BUFFER;
            if (endIndex > allItems.length) endIndex = allItems.length;

            // viewport の位置を開始インデックスに合わせてオフセットする
            viewport.style.top = (startIndex * ITEM_HEIGHT) + 'px';

            // 表示範囲のアイテムの HTML を組み立てる
            let html = '';
            for (let i = startIndex; i < endIndex; i++) {
                const item = allItems[i];
                html += '<div class="item-row">'
                    + '<span class="col-id">' + item.id + '</span>'
                    + '<span class="col-category">' + item.category + '</span>'
                    + '<span class="col-name">' + item.product_name + '</span>'
                    + '<span class="col-price">&yen;' + item.price.toLocaleString() + '</span>'
                    + '<span class="col-stock">' + item.stock + '</span>'
                    + '</div>';
            }
            // viewport に HTML を反映する
            viewport.innerHTML = html;
        }

        // スクロールイベントで再描画を実行する
        container.addEventListener('scroll', render);
        // 初期描画
        render();
    })();
</script>

IndexNoFetch.cshtml.cs
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace RazorPagesVirtualScroll.Pages
{
    public class IndexNoFetchModel : PageModel
    {
        public string DataJson { get; set; } = "";

        public void OnGet()
        {
            var items = DemoData.CreateItems();
            DataJson = JsonSerializer.Serialize(items);
        }
    }
}

解説

DataJsonプロパティに表示する項目のJSONのデータを代入して、ページ側で、JSONのデータをページモデルから取得して表示する動作になります。

実行結果

実行結果は下図です。先の実行結果と同じ動作になります。
Razor Pages で仮想スクロールを実装する:画像8 Razor Pages で仮想スクロールを実装する:画像9 Razor Pages で仮想スクロールを実装する:画像10

プロンプト

Claude Opus 4.6, Visual Studio Agent モードで以下のプロンプトを実行してコード生成できます。

Prompt1
IndexNoFetch.cshtmlにJavaScriptでFetchを利用せずに実装するデモを作成したいです。
public string DataJson { get; set; }
を定義して、ページ読み込み時にプロパティに設定することで、Fetchを利用しない仮想スクロールを実装してください。
AuthorPortraitAlt
著者
iPentecのメインプログラマー
C#, ASP.NET の開発がメイン、少し前まではDelphiを愛用
作成日: 2026-02-16