LoginSignup
12
8

BlazorでExcel ライクなグリッドJSライブラリ「Handsontable」を使ってみる

Last updated at Posted at 2022-12-15

はじめに

これは、Blazor Advent Calendar 2022の16日目の記事となります。

サーバーリプレース作業に伴い、Classic ASPやWCF(Windows Communication Foundation)で作成されたアプリケーションをBlazorとASP.NET Coreを使用して作り直そうと思っています。
Blazorを使用する上でデータグリッドコンポーネントが使用できるのかが気になりました。
UIフレームワークを見ていると表示のみや簡易的に編集可能なテーブルコンポーネントは見つかりますが、高機能で編集可能データグリッドはGrapeCity incInfragisticsの有料のものばかりです。

Handsontableとは

Handsontableは、ExcelライクなグリッドJavaScriptライブラリーとなります。
また、jQuery などの他のライブラリには依存していません。

最新バージョンは有料ですが、Handsontable 6.2.2以前のバージョンならMITライセンスが適用されます。
自分が2018年に「Handsontable Advent Calendar 2018」を主催しました。

個人的には有料でもいいなら、GrapeCity incInfragisticsのものを使用した方が、拠点が日本にありサポートも充実している上にデータグリッド以外のコンポーネントも含まれるたりするので便利かなと思ってたりします。

Blazorでの使用方法

Handsontable 6.2.2

下記サイトからZIPファイルをダウンロードしてください。
https://github.com/handsontable/handsontable/archive/refs/tags/6.2.2.zip
展開してdistフォルダにあるファイルを使用します。
なお、full版は pikaday および numbro を内蔵しています。

CDN使用

インターネットを使える環境であればCDNを使用するといいでしょう。
CDNを使用する場合、後述する「Blazorプロジェクトに配置」のところをLibs側からhttps側に書き換えてください。

_Host.cshtml
    <link href="https://cdn.jsdelivr.net/npm/handsontable@6.2.2/dist/handsontable.full.min.css" rel="stylesheet" media="screen">

    <script src="https://cdn.jsdelivr.net/npm/handsontable@6.2.2/dist/handsontable.full.min.js"></script>

Blazorプロジェクトに配置

今回はBlazor Serverでの使用方法を例にします。
※これから説明する方法は各自で変更可能です。

  1. wwwrootフォルダ配下にlibsフォルダを作成し、その配下にcssとscriptsフォルダを作成します。
  2. cssフォルダに「handsontable.full.min.css」とscriptsフォルダに「handsontable.full.min.js」を配置します。
  3. cssフォルダに「scripts.css」とscriptsフォルダに「scripts.js」を新規作成します。
  4. 「_Host.cshtml」ファイルに下記のリンクを適切な位置に追記します。
_Host.cshtml
    <link href="libs/css/handsontable.full.min.css" rel="stylesheet" />
    <link href="libs/css/scripts.css" rel="stylesheet" />

    <script src="libs/scripts/handsontable.full.min.js"></script>
    <script src="libs/scripts/scripts.js"></script>

image.png

【2022/12/23追記】
Blazorの機能を使ってグローバルスコープに現れない方法を知りました。

scripts.cssの中身

個人的に変更したいCSSを記述します。

scripts.css
.handsontable th,
.handsontable td {
    padding: 2px 10px 2px 10px;
    font-size: 16px;
    text-align: center;
}
.handsontable th:last-child {
    padding-left: 8px;
    text-align: left;
}
.handsontable td:first-child {
    background: #EEE;
}

scripts.jsの中身

scripts.js
function createGrid() {
    let data = [
        ['', false, 'S0001', 'りんご', 100, '青森産'],
        ['', false, 'S0002', 'みかん', 80, '静岡産'],
        ['*', true, 'S0003', 'メロン', 1000, '袋井クラウンメロン']
    ];

    let grid = document.getElementById('grid');
    window.hot = new Handsontable(grid, {
        data: data,
        colHeaders: ['編集', '選択', '商品CD', '商品名', '単価', '備考'],
        columns: [
            { readOnly: true, type: 'text' },
            { type: 'checkbox' },
            { type: 'text' , width: 80 },
            { type: 'text' , width: 200, className: "htLeft htMiddle" },
            { type: 'numeric', numericFormat: { pattern: '0,00', culture: 'ja-JP' }},
            { type: 'text' , width: 300, className: "htLeft htMiddle" }
        ],
        enterMoves: { row: 0, col: 1 },
        outsideClickDeselects: true,
        manualColumnResize: true,
        fillHandle: false
    });
}

【2023/03/18追記】
JavaScript を書かないで C# のみで実現している記事を見つけました。

【2023/06/13追記】
Blazor WebAssembly 専用になります。
Blazor Server では一部機能が使えますが、自分の場合は ToJS で Cast Error になったりしました。

Index.razorにグリッド用タグ配置

グリッドを表示する場所に<div id="grid"></div>をセットします。
id名は変更可能ですが、scripts.js内のgetElementById('grid')と一致させる必要があります。

Index.razor
@page "/"
@inject IJSRuntime jsRuntime

<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?" />
<div id="grid"></div>

@code {
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
         if (!firstRender) return;
      
            await jsRuntime.InvokeVoidAsync("createGrid");
    }
}

実行1回目

Handsontableの内容が表示されました。
image.png

データ読み込みについて

先程はscripts.jsに直接データを埋め込んでいますが、やはり外だしにしたいですよね。
今度はJSONデータを生成して、読み込ませるようにしてみましょう。
columnsのdataオブジェクトに列名をセットします。

C#と違ってJSONではキャメルケースが一般的なので列名はキャメルケースにしています。

これね、実はハマりました。
データは追加されているのに中身が表示されなくて空欄のまま。ログを出してキー名がキャメルケースになっていたので、constの定義名をパスカルケースからキャメルケースに変更しました。

scripts.jsの変更

scripts.js
function createGrid() {
    const COL_EDIT = 'edit';
    const COL_SELECT = 'select';
    const COL_PRODUCTCODE = 'productCode';
    const COL_PRODUCTNAME = 'productName';
    const COL_UNITPRICE = 'unitPrice';
    const COL_COMMENT = 'comment';
    const EDIT_MARK = '*';

    let grid = document.getElementById('grid');
    window.hot = new Handsontable(grid, {
        data: [],
        colHeaders: ['編集', '選択', '商品CD', '商品名', '単価', '備考'],
        columns: [
            { data: COL_EDIT, readOnly: true, type: 'text' },
            { data: COL_SELECT, type: 'checkbox' },
            { data: COL_PRODUCTCODE, type: 'text', width: 80 },
            { data: COL_PRODUCTNAME, type: 'text', width: 200, className: "htLeft htMiddle" },
            { data: COL_UNITPRICE, type: 'numeric', numericFormat: { pattern: '0,00', culture: 'ja-JP' } },
            { data: COL_COMMENT, type: 'text', width: 300, className: "htLeft htMiddle" }
        ],
        enterMoves: { row: 0, col: 1 },
        outsideClickDeselects: true,
        manualColumnResize: true,
        fillHandle: false,
        afterChange: function (changes, source) {
            if (source === 'loadData') return;
            for (var i = 0; i < changes.length; i++) {
                var change = changes[i];
                // 編集と選択は対象外
                if (change[1] === COL_EDIT || change[1] === COL_SELECT) continue;
                // 変更前と変更後が同じは対象外
                if (change[2] === change[3]) continue;
                // 編集に"*"を付ける
                hot.setDataAtCell(changes[0][0], 0, EDIT_MARK);
            }
        }
    });
}
// データセット
function loadData(data) {
    window.hot.loadData(data);
}

ポイント

Handsontableのインスタンス変数を最初にvar hot = new Handsontableとしました。
しかし、これだとloadData関数内でhot変数を使用すると「Error: Microsoft.JSInterop.JSException: hot is not defined」となってしまいます。
原因はvarが関数スコープだからです。
グローバルスコープにする必要があるため、 windows.hot = new Handsontableに変更することで動作しました。

【2022/12/23追記】
Blazorの機能を使ってよりよく改良して頂きました。

Index.razorの変更

本来はデータベースからデータを取得して表示するのですが、今回は例なのでIndex.razorにそのまま記載しています。

Index.razor
@page "/"
@inject IJSRuntime jsRuntime
@using System.Text.Json

<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?" />
<div id="grid"></div>

@code {
    public class ProductMaster
    {
        public string? Edit { get; set; }
        public bool Select { get; set; }
        public string? ProductCode { get; set; }
        public string? ProductName { get; set; }
        public int UnitPrice { get; set; }
        public string? Comment { get; set; }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (!firstRender) return;

        await jsRuntime.InvokeVoidAsync("createGrid");
        List<ProductMaster> products = new List<ProductMaster>()
        {
            { new ProductMaster() { Edit = "", Select = false, ProductCode = "S0001", ProductName = "りんご", UnitPrice = 100, Comment = "青森産" } },
            { new ProductMaster() { Edit = "", Select = false, ProductCode = "S0002", ProductName = "みかん", UnitPrice = 80, Comment = "静岡産" } },
            { new ProductMaster() { Edit = "", Select = true,  ProductCode = "S0003", ProductName = "メロン", UnitPrice = 1000, Comment = "袋井クラウンメロン" } }
        };

        await jsRuntime.InvokeVoidAsync("loadData", products);
   }
}

【2022/12/22追記】
コメントを頂きました。JSONにこだわる必要がなかったんですね。上記プログラムは変更済みです。

変更前
        string jsonString = JsonSerializer.Serialize<List<ProductMaster>>(products);
        var jsonData = JsonSerializer.Deserialize<List<ProductMaster>>(jsonString);
        await jsRuntime.InvokeVoidAsync("loadData", jsonData);
変更後
        await jsRuntime.InvokeVoidAsync("loadData", products);

【2022/12/17追記】
ボタンを追記してメッセージ(alert)を出すようにしたら、再描画した段階でHandsontableがもう一つ追加されてしまいました。
その為、if (!firstRender) return;を追加しました。最初の描画時のみHandsontableを作成する必要がありました。

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (!firstRender) return;

実行2回目

データを読み込ませて表示できました。
image.png

編集

HandsontableのafterChange関数にて、編集があった場合に「*」を付けるようにしています。
試しに金額を変更してみましょう。メロンの金額を1000円から980円にします。

image.png

最後に

これでBlazorにExcelライクなグリッドJavaScriptライブラリー「Handsontable」が使用できることが分かりました。
Handsontableの各種設定は、冒頭に紹介した「Handsontable Advent Calendar 2018」に蓄積したTipsを書いてありますので、そちらを参考にしてください。

BlazorのUIフレームワークは、最初は「MatBlazor」にしようとボタンなど書いてみたのですが、primary, secondaryなどの色指定とか定義しないと使えないのが気に食わないんですよね。
これまでBoostrapで開発してきたので、似た感じの「Ant Design Blazor」に乗り換えてしまいました。

参照

12
8
2

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
12
8