ASP.NET Core 3.1 & Vue.js 上で、Handsontableを動かしてみた

ASP.NET Core 3.1 & Vue.js 上で、Handsontableを動かす機会があったため、メモを残します。

 
目次

 

環境

 

環境構築

ASP.NET Core 3.1向けのVue.jsプロジェクトテンプレートについて

RiderなどのIDEでは、ASP.NET Coreで使えるVue.jsのテンプレートがGUIでは選択できません。

Vue向けの公式テンプレートは、dotnetコマンドでインストールできるようですが、試してみたところ、Core3.x系のテンプレートにはなりませんでした。
ASP.NET Core のテンプレートで Vue をインストール - Qiita

ほかのテンプレートを探したところ、starの多い aspnetcore-Vue-starter がありました。
TrilonIO/aspnetcore-Vue-starter: NEW Asp.net Core & Vue.js (ES6) SPA Starter kit - Vuex, webpack, Web API, Docker, and more! By @TrilonIO

ただ、こちらもCore3系には対応していませんでした。
Please Upgrade to Core 3 · Issue #138 · TrilonIO/aspnetcore-Vue-starter

Core3系に対応したテンプレートを探したところ、以下がありました。
SoftwareAteliers/asp-net-core-vue-starter: ASP.NET Core + Vue.js starter project

また、

(Optional) Scaffold Vue.js app with custom configuration

と、Vue.jsのカスタマイズもできそうでした。

そのため、このテンプレートを使うことにしました。

 

ASP.NET & Vue.jsの環境構築
テンプレートのインストール
dotnet new -i SoftwareAteliers.AspNetCoreVueStarter

 
テンプレートのリストを見ると、他のVue.js向けテンプレートをインストールしていたせいか、Short Nameが重複してしまいました。

そのため、テンプレートを使う時は、 Templatesに記載されている .NET Core Vue.js を指定します。

$ dotnet new list

Templates                                         Short Name
------------------------------------------------------------
...
ASP.NET Core with Vue.js                          vue       
...                         
.NET Core Vue.js                                  vue       

 

プロジェクトの作成
# ソリューションファイルを作成
$ dotnet new sln -n HandsonTableVueOnAspNetCore

# テンプレート名を使って、プロジェクトを作成
$ dotnet new ".NET Core Vue.js" -o HandsonTableVueOnAspNetCore

# プロジェクトをソリューションに追加
$ dotnet sln add ./HandsonTableVueOnAspNetCore
プロジェクト `HandsonTableVueOnAspNetCore/HandsonTableVueOnAspNetCore.csproj` をソリューションに追加しました。

# Vue.jsのカスタマイズをするため、READMEにある通り、ClientAppを削除
$ rm -rf ClientApp/

 

Vue CLIによる、Vue.jsの環境構築

READMEに従い、 client-app という名前で生成しました。

ひとまずRouterだけ追加しておきます。

$ vue create client-app
Vue CLI v4.1.2
? Please pick a preset: Manually select features
? Check the features needed for your project: Router
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No

 

TypeScriptの追加

client-appに対し、TypeScriptを追加します。

ただし、今回はTypeScript化までは行わないため、不要な場合はパスしても大丈夫です。

現時点では、Handsontable向けには、TypeScriptは3.6系を使っておくのが良さそうなので、バージョン指定してインストールします。
Import declaration conflicts with local declaration of 'HotTableProps'. · Issue #145 · handsontable/vue-handsontable-official

$ cd client-app/
$ npm install --save-dev typescript@3.6.4
+ typescript@3.6.4

 
続いて、Vue.jsに追加します。

TSLintの代わりにESLintを使うため、TSLint以外はデフォルトのままで進めます。

ワーニングが出ますが、とりあえずこのままで進めます。

$ vue add typescript
? Still proceed? Yes

✔  Successfully installed plugin: @vue/cli-plugin-typescript

? Use class-style component syntax? Yes
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
? Use TSLint? No
? Convert all .js files to .ts? Yes
? Allow .js files to be compiled? No

WARN  conflicting versions for project dependency "typescript":

- ^3.6.4 injected by generator "undefined"
- ~3.5.3 injected by generator "@vue/cli-plugin-typescript"

Using newer version (^3.6.4), but this may cause build errors.

 

ESLintの追加
$ npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-vue
+ eslint@6.8.0
+ eslint-plugin-vue@6.1.2
+ @typescript-eslint/parser@2.14.0
+ @typescript-eslint/eslint-plugin@2.14.0

 

Prettier
$ npm install --save-dev prettier eslint-plugin-prettier eslint-config-prettier
+ prettier@1.19.1
+ eslint-config-prettier@6.9.0
+ eslint-plugin-prettier@3.1.2

 

client-app内にある、不要なgitリポジトリを削除

今回はASP.NET Coreも含めてリポジトリ管理するため、削除しておきます。

$ rm -rf .git/

 

ディレクトリの名前を変更

READMEにある通り、 client-app から ClientApp へと変更します。

$ cd ..
$ mv client-app/ ClientApp/

 

動作確認

dotnet run コマンドで起動し、ASP.NET Core & Vue.jsが動作していることを確認します。

$ dotnet run

f:id:thinkAmi:20200105230500p:plain:w400

 

Vue.js & Handsontableの環境構築
vue-handsontable-officialのインストール

Vue.js向けとして、Handsontable公式が vue-handsontable-official を提供しています。
handsontable/vue-handsontable-official: Vue Data Grid with Spreadsheet Look & Feel. Official Vue wrapper for Handsontable.

そのため、インストールします。

$ npm install handsontable @handsontable/vue
...
Handsontable is no longer released under the MIT license. Read more about this change on our blog at https://handsontable.com/blog.
+ handsontable@7.3.0
+ @handsontable/vue@4.1.1

 

実装
Handsontableを使ったコンポーネントを作成

ClientApp/src/components/HelloHandsontable.vue としてコンポーネントを作成します。

<template>
    <div>
        <hot-table :settings="hotSettings" />
    </div>
    
</template>

<script>
    import {HotTable} from "@handsontable/vue";
    
    export default {
        name: "HelloHandsontable",
        components: {
            HotTable
        },
        data() {
            return {
                hotSettings: {
                    // 非商用向けのライセンス
                    licenseKey: 'non-commercial-and-evaluation',
                    
                    data: [
                        [1, "紅あずま", 10],
                        [2, "紅はるか", 20],
                        [3, "シルクスイート", 30],
                    ],
                    colHeaders: ["No", "Name", "Price"],
                    rowHeaders: ["1st", "2nd", "3rd"],
                    
                    // コンテキストメニューまわり
                    // contextMenu: true,  // trueにすると、ブラウザのコンテキストメニューが表示されない
                    allowInsertColumn: false,
                    allowRemoveColumn: false,
                    
                    // 列のソートインディケータを表示
                    columnSorting: {
                        indicator: true
                    }
                }
            }
        }
    }
</script>

<style>
    @import '~handsontable/dist/handsontable.full.css';

    /* 列ヘッダの色を変更する */
    .handsontable thead th .relative {
        background-color: deepskyblue;
    }
</style>

 

App.vueの修正

templateタグの差し替えと、script内にコンポーネントを追加します。

<template>
  <div id="app">
    <!-- Handsontableの表示へと変更
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>
    -->
    <div>
      <h3>Hello Handsontable</h3>
      <HelloHandsontable />
    </div>
  </div>
</template>

<script lang="ts">
// 追加
import HelloHandsontable from "@/components/HelloHandsontable.vue";

@Component({
  components: {
    HelloHandsontable,  // 追加
    HelloWorld,
  },
})
export default class App extends Vue {}
</script>

 

開発証明書の適用

もし開発証明書の信頼を行っていない場合、行っておきます。
Windows および macOS で ASP.NET Core HTTPS 開発証明書を信頼する | ASP.NET Core に HTTPS を適用する | Microsoft Docs

$ dotnet dev-certs https --trust

 

動作確認

ここまでで、ASP.NET Core & Vue.js & Handsontableの環境ができましたので、動作を確認します。

dotnet run コマンドで起動します。

$ dotnet run

 
https://localhost:5000 へアクセスし、以下が表示されればOKです。

f:id:thinkAmi:20200105230555p:plain:w400

 

ASP.NET CoreでJSONを返すAPIの作成と連携

まずは、定形JSONを返すASP.NET Core APOIを作成し、そのレスポンスをHandsontableへと反映させてみます。

 

Vueコンポーネントの作成

先ほどと同じようなコンポーネントを作成します。

違いとしては、

  • hotSettingsに渡す data の初期値は、空の配列にする
  • created()で、APIからJSONを受け取り、Handsontableに反映する

となります。

<template>
    <div>
        <hot-table :settings="hotSettings" />
    </div>
    
</template>

<script>
    import {HotTable} from "@handsontable/vue";
    
    export default {
        name: "ApiResponseHandsontable",
        components: {
            HotTable
        },
        data() {
            return {
                hotSettings: {
                    // 非商用向けのライセンス
                    licenseKey: 'non-commercial-and-evaluation',
                    
                    // 初期データなし
                    data: [],
                    // Name列だけ、幅を指定する
                    colWidths: [null, 200, null],
                    
                    colHeaders: ["No", "Name", "Price"],
                    rowHeaders: ["1st", "2nd", "3rd"],
                    
                    // コンテキストメニューまわり
                    // contextMenu: true,  // trueにすると、ブラウザのコンテキストメニューが表示されない
                    allowInsertColumn: false,
                    allowRemoveColumn: false,
                    
                    // 列のソートインディケータを表示
                    columnSorting: {
                        indicator: true
                    }
                }
            }
        },
        created: function() {
            // ロードされた時にAPIを呼んで、Handsontableの初期値を取得する
            fetch('/api/const')
                .then(res => { return res.json() })
                .then(data => {
                    this.hotSettings.data = JSON.parse(data);
                })
        },
    }
</script>

<style>
    @import '~handsontable/dist/handsontable.full.css';

    /* 列ヘッダの色を変更する */
    .handsontable thead th .relative {
        background-color: deepskyblue;
    }
</style>

 

App.vueの修正

コンポーネントを追加します。

<template>
  <div id="app">
    <!-- Handsontableの表示へと変更
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>
    -->
    <div>
      <h3>Hello Handsontable</h3>
      <HelloHandsontable />
    </div>
    
    <hr>
    
    <div>
      <h3>Const Response Handsontable</h3>
      <ConstResponseHandsontable />
    </div>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
//...
// 追加
import ConstResponseHandsontable from "@/components/ConstResponseHandsontable.vue";

@Component({
  components: {
    ConstResponseHandsontable,  // 追加
    HelloHandsontable,
    HelloWorld,
  },
})
export default class App extends Vue {}
</script>

 

ASP.NET Coreのコントローラーを作成

/api/const にアクセスしたときにJSONレスポンスを返すコントローラーを作成します。

コントローラークラスに

  • ControllerBaseを継承
  • [Route("api/")] 属性で、APIのルーティングを設定
  • [ApiController] 属性で、API向けコントローラーとして動作するよう設定
  • [Produces(MediaTypeNames.Application.Json)] 属性で、レスポンスをJSONにするように指定

とします。

あとは、 GetConstResponse() メソッドに、JSONでレスポンスする内容を記載します。

 
なお、Handsontableのデータ投入メソッド(loadData())に不具合があるようで、7.3.0時点では配列の配列でしかデータを投入できないようです。
Using loadData on an object data doesn't work · Issue #4204 · handsontable/handsontable

8系がリリースされるとオブジェクト配列で投入できそうなので、その時は以下のコードは書き換えたほうが良いかもしれません。

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net.Mime;
using System.Text.Json;
using Bogus;
using HandsonTableVueOnAspNetCore.Models;
using Microsoft.AspNetCore.Mvc;

namespace HandsonTableVueOnAspNetCore.Controllers
{
    [Route("api/")]
    [ApiController]
    [Produces(MediaTypeNames.Application.Json)]
    public class HandsontableApiController : ControllerBase
    {
        [HttpGet("const")]
        public IActionResult GetConstResponse()
        {
            var apples = new List<dynamic>
            {
                new List<dynamic> {1, "秋映", 100},
                new List<dynamic> {2, "シナノゴールド", 200},
                new List<dynamic> {3, "ピンクレディ", 300}
            };
            return Ok(JsonSerializer.Serialize(apples));
        }
    }
}

 

動作確認

まずはAPIの動作を確認します。

localhost:5000/api/const にアクセスすると、JSONが返ってくればOKです。

$ curl http://localhost:5000/api/const
"[[1,\"\\u79CB\\u6620\",100],[2,\"\\u30B7\\u30CA\\u30CE\\u30B4\\u30FC\\u30EB\\u30C9\",200],[3,\"\\u30D4\\u30F3\\u30AF\\u30EC\\u30C7\\u30A3\",300]]

 

続いて、 localhost:5000 にアクセスし、JSONの内容がHandosontableに反映されていればOKです。

f:id:thinkAmi:20200105230523p:plain:w400

 

ASP.NET CoreでモデルのJSONを返すAPIの作成と連携

最後に、ASP.NET CoreのモデルをHandsontableに表示してみます。

 

環境構築

今回はEntityFramework Core & SQLiteを使うため、必要なパッケージを追加します。

dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SQLite

また、念のため、発行されたSQLの中身もコンソール出力するため、パッケージを追加します。

dotnet add package Microsoft.Extensions.Logging.Console

 

Vue.jsのコンポーネントを作成

create()でアクセスするAPIのエンドポイントが変更となっただけで、あとは同じです。

<template>
    <div>
        <hot-table :settings="hotSettings" />
    </div>
    
</template>

<script>
    import {HotTable} from "@handsontable/vue";
    
    export default {
        name: "ModelResponseHandsontable",
        components: {
            HotTable
        },
        data() {
            return {
                hotSettings: {
                    // 非商用向けのライセンス
                    licenseKey: 'non-commercial-and-evaluation',
                    
                    // 初期データなし
                    data: [],
                    // Name列だけ、幅を指定する
                    colWidths: [null, 200, null],
                    
                    colHeaders: ["No", "Name", "Age"],
                    rowHeaders: ["1st", "2nd", "3rd"],
                    
                    // コンテキストメニューまわり
                    // contextMenu: true,  // trueにすると、ブラウザのコンテキストメニューが表示されない
                    allowInsertColumn: false,
                    allowRemoveColumn: false,
                    
                    // 列のソートインディケータを表示
                    columnSorting: {
                        indicator: true
                    }
                }
            }
        },
        created: function() {
            // ロードされた時にAPIを呼んで、Handsontableの初期値を取得する
            fetch('/api/model')
                .then(res => { return res.json() })
                .then(data => {
                    this.hotSettings.data = JSON.parse(data);
                })
        },
    }
</script>

<style>
    @import '~handsontable/dist/handsontable.full.css';

    /* 列ヘッダの色を変更する */
    .handsontable thead th .relative {
        background-color: deepskyblue;
    }
</style>

 

App.vueの修正

こちらも、コンポーネントを追加するだけです。

<template>
  <div id="app">
    <!-- Handsontableの表示へと変更
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>
    -->
    <div>
      <h3>Hello Handsontable</h3>
      <HelloHandsontable />
    </div>
    
    <hr>
    
    <div>
      <h3>Const Response Handsontable</h3>
      <ConstResponseHandsontable />
    </div>
    
    <hr>
    
    <div>
      <h3>Model Response Handsontable</h3>
      <ModelResponseHandsontable />
    </div>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import HelloWorld from './components/HelloWorld.vue';
import HelloHandsontable from "@/components/HelloHandsontable.vue";
import ConstResponseHandsontable from "@/components/ConstResponseHandsontable.vue";

// 追加
import ModelResponseHandsontable from "@/components/ModelResponseHandsontable.vue";

@Component({
  components: {
    ModelResponseHandsontable,  // 追加
    ConstResponseHandsontable,
    HelloHandsontable,
    HelloWorld,
  },
})
export default class App extends Vue {}
</script>

 

モデルの作成

Models/Customer.cs として作成します。

namespace HandsonTableVueOnAspNetCore.Models
{
    public class Customer
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Age { get; set; }
    }
}

 

DbContextの作成

Models/HandsontableContext.cs として、

  • ログをコンソールに出力
  • SQLiteを使う

のDbContextを作成します。

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace HandsonTableVueOnAspNetCore.Models
{
    public class HandsontableContext : DbContext
    {
        public HandsontableContext(DbContextOptions<HandsontableContext> options) : base(options) {}

        public DbSet<Customer> Customers { get; set; }

        public static readonly ILoggerFactory MyLoggerFactory
            = LoggerFactory.Create(builder =>
            {
                builder
                    .AddFilter(DbLoggerCategory.Database.Command.Name, LogLevel.Information)
                    .AddConsole();
            });
        
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            base.OnConfiguring(optionsBuilder);
            optionsBuilder
                .EnableSensitiveDataLogging()
                .UseLoggerFactory(MyLoggerFactory);
        }
    }
}

 

appsettings.Development.jsonの修正

SQLite向けの接続文字列を追加します。

今回は開発環境なので、 appsettings.Development.json ファイルに追加します。

{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  },

  // 以下を追加
  "ConnectionStrings": {
    "HandsontableContext": "Data Source=./WebApplication.db"
  }
}

 

Startup.csの修正

ConfigureServicesに、DbContextを追加します。

public void ConfigureServices(IServiceCollection services)
{
    //...

    // 追加
    services.AddDbContext<HandsontableContext>(options => options.UseSqlite(
        Configuration.GetConnectionString("HandsontableContext")));
}

 

マイグレーション

モデルとDbContextができたため、マイグレーションを行います。

# マイグレーションファイルを作成
$ dotnet ef migrations add InitialCreate

# SQLiteへ反映
$ dotnet ef database update

 

コントローラーの修正
Seed用URLを作成

デフォルトデータとして投入する方法として以前は OnModelCreating() をオーバーライドしました。
データシード処理-EF Core | Microsoft Docs

ただ、今回は何度でも使えるよう、Seed用URLを作成します。
c# - How to seed in Entity Framework Core 2? - Stack Overflow

また、デフォルトデータはランダムなもので良いので、 Bogus を使います。
bchavez/Bogus: A simple and sane fake data generator for C#, F#, and VB.NET. Based on and ported from the famed faker.js.

パッケージをインストールします。

$ dotnet add package Bogus

あとは、コントローラーでDbContextを受け取り、Seed処理を実装します。

GETメソッドでのSeedでいいのか感はありますが、開発用途なのでヨシとします。

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net.Mime;
using System.Text.Json;
using Bogus;
using HandsonTableVueOnAspNetCore.Models;
using Microsoft.AspNetCore.Mvc;

namespace HandsonTableVueOnAspNetCore.Controllers
{
    [Route("api/")]
    [ApiController]
    [Produces(MediaTypeNames.Application.Json)]
    public class HandsontableApiController : ControllerBase
    {
        // 追加
        private readonly HandsontableContext _context;
        public HandsontableApiController(HandsontableContext context)
        {
            this._context = context;
        }
        
        //...

        // 追加
        [HttpGet("seed")]
        public IActionResult Seed()
        {
            var faker = new Faker<Customer>("ja")
                .RuleFor(r => r.Name, f => $"{f.Name.LastName()} {f.Name.FirstName()}")
                .RuleFor(r => r.Age, f => f.Random.Number(20, 60));

            var fakes = faker.Generate(3);
            
            _context.Customers.AddRange(fakes.ToArray());
            _context.SaveChanges();

            var customers = fakes.Select(a => new ArrayList
            { 
                a.Id, a.Name, a.Age
            });

            return Ok(JsonSerializer.Serialize(customers));
        }
    }
}

 

モデルからデータを読み込んでJSONレスポンスするAPI作成

同じくコントローラーに追加します。

/api/modelJSONレスポンスを行います。

[HttpGet("model")]
public IActionResult GetApplesResponse()
{
    var response = _context.Customers.Select(c => new ArrayList
    {
        c.Id, c.Name, c.Age
    });
    
    return Ok(JsonSerializer.Serialize(response));
}

 

動作確認

データベースへのSeedを行います。

$ curl http://localhost:5000/api/seed
"[[1,\"\\u9AD8\\u6A4B \\u9678\\u6597\",40],[2,\"\\u677E\\u672C \\u7F8E\\u7FBD\",56],[3,\"\\u658E\\u85E4 \\u592A\\u4E00\",52]]"

 
モデルAPIのレスポンスも確認します。

$ curl http://localhost:5000/api/model
"[[1,\"\\u9AD8\\u6A4B \\u9678\\u6597\",40],[2,\"\\u677E\\u672C \\u7F8E\\u7FBD\",56],[3,\"\\u658E\\u85E4 \\u592A\\u4E00\",52]]"

 
最後に localhost:5000 にアクセスし、表示できればOKです。 (中身はBogusによるダミーデータです)

f:id:thinkAmi:20200105230624p:plain:w400

 

ソースコード

Githubに上げました。
thinkAmi-sandbox/Handsontable_On_ASP_NET_Core_Vue-sample