Shoeisha Technology Media

CodeZine(コードジン)

記事種別から探す

実践! SpreadJSとASP.NET MVCで帳票Webアプリを作成してみよう

ASP.NET MVCとSpreadJSで業務Webアプリケーションモダン化計画 第3回

  • LINEで送る
  • このエントリーをはてなブックマークに追加
2016/12/08 14:00

 ExcelライクなJavaScriptライブラリ「SpreadJS」と「ASP.NET MVC」の組み合わせで、"請求書"を管理する帳票Webアプリを作成します。データの保存・読み込みや、Excelファイルの入出力、印刷機能を実装します。

はじめに

 第1、2回では、「SpreadJS」の基本的な使い方や、サーバー側のWebフレームワークとしてASP.NET MVCの現状を確認しました。

 本稿では、SpreadJSとサーバー側を連携し、実際にとあるシナリオを想定して帳票Webアプリを作成します。サンプルの一部を抜粋しながら、以下の機能がどのように実装できるか、紹介します。

想定シナリオ

  • "請求書"をブラウザ上で表示・編集し、データをサーバー側に保存する。
  • 作成した"請求書"を再度読みこむ。
  • 作成した"請求書"を、印刷・Excelファイルとして出力する。

構成

  • サーバー側 Web フレームワーク:ASP.NET MVC、ASP.NET Web API
  • クライアント側:TypeScript、SpreadJSライブラリ
  • IDE:Visual Studio 2015

作成する帳票Webアプリの図

数量や単価を変更することで、小計や合計も自動的に計算されます。中間はテーブルとして定義し、上下数行を固定化しました。
数量や単価を変更することで、小計や合計も自動的に計算されます。中間はテーブルとして定義し、上下数行を固定化しました。
テーブルなので並べ替えもできます。
テーブルなので並べ替えもできます。
サンプルでは、SpreadJSの操作をHTML要素のボタンから行います。
サンプルでは、SpreadJSの操作をHTML要素のボタンから行います。

対象読者

  • ASP.NET、JavaScriptを利用する開発者
  • スプレッドシートの機能に興味がある方
  • Excelのような帳票Webアプリを作成したい方

必要な環境

 SpreadJS 9J の必要システムを参照。

 本稿では、次の環境で開発・動作確認を行っています。

  • Chrome
  • Visual Studio 2015
  • Windows 10

サンプルについて

 サンプルにはSpreadJSライブラリは含まれておりません。ご利用いただく場合には、別途SpreadJSのトライアル版(または製品版)をダウンロードする必要があります。ダウンロードした後、サンプル内のREADME.txtに従ってファイルを配置してください。

既存のExcelファイルをSpreadJSに適用する

 はじめに、帳票のデザインをSpreadJSで表示するにはどのようにしたらよいでしょうか。例えば、サンプルの請求書のデザインをSpreadJSに設定したいとします。

 SpreadJSのデザインはJavaScriptの実装より行うことができますが、コード量が多くなり複雑になる可能性があります。そこで、すでにデザインされたExcelファイルを利用し、SpreadJSをあらかじめシリアル化しておくことで、アプリケーションの主となるロジックと区別することができます。

 本稿のサンプルでは、製品ヘルプの「Excelのインポート機能」と「SpreadJSのシリアル化・逆シリアル化」を利用し、次の方法を選択しました。

  1. デザインされたExcelファイルをSpreadJSに読み込む。
  2. SpreadJSをシリアル化したJSONデータを「*.json」ファイルとして出力する。
  3. 作成した「*.json」ファイルをサーバー側に配置する。
  4. ブラウザ側で、初期表示に「*.json」ファイルの内容をSpreadJSに読み込む。

 まず、(1)のExcelファイルをSpreadJSに読み込むための、クライアント側のTypeScriptの記述です。

// Excelファイルをインポートします
export function ImportExcelFile() {
    let input: any = document.getElementById("fileDemo");
    let excelFile = input.files[0];
    excelIO.open(excelFile, function (json) {
        var workbookObj = JSON.parse(json);
        workbook.fromJSON(workbookObj);
    }, e => { console.log(e); });
}

 このクライアント側のExcelインポート機能については、連載第2回の「クライアントサイドのExcelインポート・エクスポート」でも紹介しておりますのでご参照ください。

 次に、(2)のSpreadJS全体の設定をJSONデータとしてシリアル化するTypeScriptの記述です。

//SpreadJSの初期化
var workbook = new GC.Spread.Sheets.Workbook(document.getElementById("spreadjs-container"), {
            sheetCount: 1
        });
//JSONデータとして出力する
let json = JSON.stringify(workbook.toJSON({ includeBindingSource: false }));

 詳細は、チュートリアルデモの「JSONによる初期化と保存」をご覧ください。

 SpreadJSのシリアル化されたJSONデータは次のようになっています(一部のみ記載しています)。

{
    "version": "9.20161.0",
    "sheets": {
        "Sheet1": {
            "name": "Sheet1",
            "frozenRowCount": 10,
            "frozenTrailingRowCount": 12,
            "rowCount": 33,
            "columnCount": 4,
            "activeRow": 4,
            "activeCol": 2,
            "spans": [
                {
                    "row": 32,
                    "rowCount": 1,
                    "col": 0,
                    "colCount": 4
                },
//..続く

 以上の手順で取得したJSONデータを「*.json」ファイルとして出力し、サーバー側のASP.NET MVCプロジェクトの「App_Data/spread」フォルダ配下に配置します。

 この「*.json」ファイルは、初期表示の際にサーバー側のAPIを通してクライアント側で取得し、SpreadJSに読み込みます。

 「*.json」ファイルの中身を返すASP.NET Web APIの実装は次のようになります。

/// はじめに表示するSpreadJSのコンテンツを取得します
[HttpGet]
public HttpResponseMessage ReadSpread()
{
    string fileName = "initialSpread.json";
    string filePath = Path.Combine(spreadFolder, fileName);

    var response = new HttpResponseMessage(HttpStatusCode.OK);
    var fileStream = new FileStream(filePath, FileMode.Open);  
    response.Content = new StreamContent(fileStream);
    response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");

    return response;
}

 ASP.NET Web APIで提供するAPIではなく、直接Webサーバー側のファイルを取得する方法でも構いません。ただし、*.jsonファイルが外部に公開されることに注意してください。またIISでは、規定では「*.json」ファイルの送信は許可されていませんので、MIMEマッピングを追加する必要があります。

 クライアント側でAPIリクエストを送信し、「*.json」ファイルの中身を読み込み、SpreadJSに適用します。

function LoadWorkbook() {
        
    //初期表示のSpreadJSを読み込みます
    $.ajax({
        url: "/api/spread/",
        datatype: "json",
        cache: false,
        success: (data) => {
            workbook.suspendPaint();
            workbook.fromJSON(data);
            let sheet = workbook.getSheet(0);
            //シートに対してセットアップを行います。
            SetupSheet(sheet);
            workbook.resumePaint();
        },
        error: (ex) => {
            console.log(ex);
        }
    } as JQueryAjaxSettings);
}

 以上で、デザインされたExcelファイルをSpreadJSに適用できます。作成したExcelファイルはサンプル内にありますのでご参照ください。

データを保存する(1)

 データの保存といえども、"とある値のみ保存する"のか、"帳票全体を保存"するのか等、決定しなければいけません。サンプルでは両方を実装していますが、まず、前者の"とある値のみ保存する"方法について紹介します。

管理するデータをJSONデータとして定義する

 サーバー側で管理したいデータを帳票から切り出し、JSON形式のモデルとして定義します。例えば"請求書"では、次の図のように切り分けました。

 赤枠がJSON形式のモデルとして定義する場所です。青枠は、Excelの数式のように、表示するときに計算される場所です。この青枠は、JSONモデルとして定義はしません(ただし、要件によっては、計算結果もサーバー側で管理することも考えられます)。

 切り出したJSONモデルは次のようになります。

{
    "publishDate": "2016-12-01",
    "paymentDate": "2016-12-20",
    "invoiceNumber": 10003,
    "discount": 500,
    "destination": {
        "name": "杉本 昭 様",
        "companyName": "杉本 昭 事務所",
        "address1": "東京都",
        "address2": "港区"
    },
    "publisher": {
        "name": "山田 太郎",
        "companyName": "山田 太郎 事務所",
        "address1": "東京都",
        "address2": "中央区"
    },
    "paymentInfo": {
        "name": "山田 太郎",
        "nameKana": "ヤマダタロウ",
        "bankName": "銀行名",
        "bankAccount": "1234567"
    },
    "items": [
        { "id": 1, "amount": 11, "name": "項目名1", "unitPrice": 1500, "sumPrice": 0 },
        { "id": 2, "amount": 13, "name": "項目名2", "unitPrice": 2500, "sumPrice": 0 },
        //…続く
    ]
}

 このようなJSONデータをサーバー側とAPIを通してやり取りすることにより、データの管理を行います。

 SpreadJSにJSONデータを表示するには、「セルレベルバインディング」と「テーブルバインディング」の機能で実装します。バインディングにより、SpreadJSで編集した値は、クライアント側のObjectにも反映されます。詳細は、製品ヘルプの「データの連結」または連載第2回の「基本的な使い方(2)」をご参照ください。

 次のコードは、バインディングの設定を行っているクライアント側のTypeScriptのコードです。JSONデータとSpreadJSの表示場所の関連付けを行い、テーブルを追加しています。テーブルはExcelファイルにて設定していても、Excelのインポート時に破棄されてしまうため、TypeScriptにて設定しています。

function BindSheet(sheet: GC.Spread.Sheets.Worksheet, data: any) {

    let tableDataRowCount = data.items.length;

    //請求書番号
    sheet.setBindingPath(0, 1, "invoiceNumber");
    //発行日
    sheet.setBindingPath(1, 1, "publishDate");
    //支払期日
    sheet.setBindingPath(2, 1, "paymentDate");
    //宛先
    sheet.setBindingPath(4, 0, "destination.name");
    sheet.setBindingPath(5, 0, "destination.companyName");
    sheet.setBindingPath(6, 0, "destination.address1");
    sheet.setBindingPath(7, 0, "destination.address2");
    //発行者
    sheet.setBindingPath(4, 2, "publisher.name");
    sheet.setBindingPath(5, 2, "publisher.companyName");
    sheet.setBindingPath(6, 2, "publisher.address1");
    sheet.setBindingPath(7, 2, "publisher.address2");
    //割引
    sheet.setBindingPath(10 + tableDataRowCount, 3, "discount");
    sheet.getCell(1, 1)
    //支払情報
    sheet.setBindingPath(16 + tableDataRowCount, 1, "paymentInfo.name");
    sheet.setBindingPath(17 + tableDataRowCount, 1, "paymentInfo.nameKana");
    sheet.setBindingPath(18 + tableDataRowCount, 1, "paymentInfo.bankName");
    sheet.setBindingPath(19 + tableDataRowCount, 1, "paymentInfo.bankAccount");
        
    //シートにテーブルをバインドします
    let amountInfo = new GC.Spread.Sheets.Tables.TableColumn("数量");
    amountInfo.name("数量");
    amountInfo.dataField("amount");
    let nameInfo = new GC.Spread.Sheets.Tables.TableColumn("項目");
    nameInfo.name("項目");
    nameInfo.dataField("name");
    let unitPriceInfo = new GC.Spread.Sheets.Tables.TableColumn("単価");
    unitPriceInfo.name("単価");
    unitPriceInfo.dataField("unitPrice");
    let sumPriceInfo = new GC.Spread.Sheets.Tables.TableColumn("金額");
    sumPriceInfo.name("金額");
    sumPriceInfo.dataField("sumPrice");

    let table = sheet.tables.addFromDataSource("TableDetails", 9, 0, data.items, GC.Spread.Sheets.Tables.TableThemes.medium23);
    table.bindColumns([amountInfo, nameInfo, unitPriceInfo, sumPriceInfo]);

    //金額の列の計算式を設定します
    table.setColumnDataFormula(3, "=[@単価]*[@数量]");
    //小計の計算式を設定します
    sheet.getCell(11 + tableDataRowCount, 3).formula("=SUM(TableDetails[金額])-Discount");

    //シートにデータをバインディングします
    sheet.setDataSource(new GC.Spread.Sheets.Bindings.CellBindingSource(itemData));
}

 「数式」や「名前の定義」は、Excelインポートを行っても保持されるので、あらかじめExcelにて設定しておくとよいでしょう。サンプルでは、「割引」や「小計」「税」などに名前を設定しています。

Excelにて「数式」「名前の定義」を設定している図

 このようなバインディング機能により、JSONデータとSpreadJSの表示を分離して開発を行うことができます。

データを保存する(2)

JSONデータの保存と読み込み

 前節のように定義したJSONデータは、サーバー側から取得し、SpreadJSに表示して編集した後、サーバーに送信することで管理します。サンプルでは、JSONデータの保存方法として「サーバー側のローカルにてファイルを保存する」方法を選択しました。この他にも、サーバー側にてJSONデータを逆シリアル化し、Microsoft SQL ServerやRedisなど、さまざまな永続化の方法を選択できます。技術資料の「ASP.NET MVCでの使い方」では、ADO.NET Entity Data Modelでの実装があります。

 次のコードは、サーバー側のASP.NET Web APIにて、初期データを提供する実装です。

/// <summary>
/// 初期表示に表示するデータ initial.json を取得します
/// </summary>
/// <returns></returns>
[HttpGet, Route("initial")]
public HttpResponseMessage GetInitialItem()
{
    string fileName = "initialItem.json";
    var filePath = Path.Combine(dataFolder, fileName);

    var response = new HttpResponseMessage(HttpStatusCode.OK);
    var fileStream = new FileStream(filePath, FileMode.Open);
    response.Content = new StreamContent(fileStream);
    response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json);
    return response;
}

 JSONデータをクライアント側で読み込み、SpreadJSにバインディングします。

 //シートのセットアップを行います
function SetupSheet(sheet: GC.Spread.Sheets.Worksheet) {

    $.ajax({
        url: "/api/item/initial",
        datatype: "json",
        cache: false,
        success: data => {

                //シートにデータをバインドします
                BindSheet(sheet, itemData);
                //列の自動生成をOFFにします
                sheet.autoGenerateColumns = false;
                //上下の行を固定します。
                sheet.frozenRowCount(9);
                sheet.frozenTrailingRowCount(12);
        },
        error: (XMLHttpRequest, textStatus, errorThrown) => {
            console.log(errorThrown)
        }
    } as JQueryAjaxSettings);

}

 例として、次のようにテーブルの行を編集します。

 このとき、JavaScriptのオブジェクトも変更されている状態になります。

 さらに続けて、テーブルに行を追加してみましょう。行を追加するコードは以下のようになります。この場合もJavaScriptのオブジェクトに1行追加された状態になります。

//SpreadJSの行を追加します
export function AddItem() {
    var sheet = workbook.getActiveSheet();
    //現在選択している行を取得します
    var activeRowIndex = sheet.getActiveRowIndex();
    //選択行の下に1行追加します。
    sheet.addRows(activeRowIndex + 1, 1);
}

 保存する際は、JSONデータをAPIリクエストとしてサーバー側へ送信します。

//データをuser1として保存します
export function SaveData() {
    $.ajax({
        url: "api/item/user1",
        type: "POST",
        data: JSON.stringify(itemData),
        dataType: "json",
        contentType: "application/json,charset=UTF-8"
    });
}

 サーバー側のASP.NET Web APIでは、送信されてきたJSONデータをファイルに保存しています。

/// データをJSONファイルとして、user1.json の名前で保存します
[HttpPost, Route("user1")]
public async Task Save()
{
    string fileName = "user1.json";
    var filePath = Path.Combine(dataFolder, fileName);

    //リクエストのBodyからJSONデータを読み込みます
    var stream = await Request.Content.ReadAsStreamAsync();
//省略

 これまで説明してきたように、管理したいデータをJSONデータとして定義し、SpreadJSにはバインディングで表示を制御し、サーバー側とはAPIでやり取りすることによりデータを管理できます。また、同じような方法で、サーバー側にSpreadJSのシリアル化データを保存することで、帳票全体の管理が可能です。

 サンプルでは実装していませんが、例えば、宛先や銀行情報はマスタデータから読み込むといったことも、JSONデータを操作するだけで実現できます。SpreadJSがサーバーと分離されたクライアントライブラリだからこそ、柔軟に扱うことができます。

印刷

 SpreadJSでは印刷をサポートしています。詳細は製品ヘルプの「印刷の概要」をご参照ください。サンプルでは、HTMLのボタンを用意し、印刷機能を実装しました。

 クライアント側の印刷の実装は次のようになります。

//印刷します
export function Print() {
    let printSheetIndex = 0;

    var info = workbook.getSheet(printSheetIndex).printInfo() as GC.Spread.Sheets.Print.PrintInfo;
    info.showColumnHeader(GC.Spread.Sheets.Print.PrintVisibilityType.hide);    info.showRowHeader(GC.Spread.Sheets.Print.PrintVisibilityType.hide);
    info.showGridLine(false);
    info.showBorder(false);

    workbook.print(printSheetIndex);
}

 下の図は、Chromeブラウザで印刷を実行したときの図です。

 保存状態に関わらず、そのままのSpreadJSの状態で印刷を行うことができます。

最後に

 連載の最終回である本稿では、実践的な内容として、"請求書"を例にした帳票Webアプリの作成をご紹介しました。

 今回のポイントとなる機能は以下の通りです。

  • クライアント側Excelインポート・エクスポート
  • JSONデータとSpreadJSとのバインディング
  • SpreadJSのシリアル化・逆シリアル化
  • 印刷機能
  • TypeScriptによる記述

 これらのポイントと、サーバーとクライアントの分離を意識した設計をおさえると、柔軟性が高い、表機能のWebアプリに一歩近づきます。連載でご紹介した機能以外にも、さまざまな機能がありますので、ぜひSpreadJSサイトをご覧ください。

  • LINEで送る
  • このエントリーをはてなブックマークに追加

著者プロフィール

  • 矢後 比呂加(ヤゴ ヒロカ)

     Microsoft MVP for Visual Studio and Development Technologies(https://mvp.microsoft.com/ja-jp/PublicProfile/5000246)  シグマコンサルティング株式会社にて、Microsoft Azu...

おすすめ記事

All contents copyright © 2006-2017 Shoeisha Co., Ltd. All rights reserved. ver.1.5