【.NET】簡易ExcelファイルをHTMLに変換して表示する

# はじめに
同僚がデータ修正ツールを作成する上で、修正前のデータと修正後のデータをExcelファイルに出力して表示していた。
社内で使用するだけなら、Office 365がインストールされているPCで行うのでいいのですが、実際に使用してもらうユーザーの環境には場所（工場内など）によってはOfficeがインストールされてないPCがあるため、レビューでExcelファイルをHTMLに変換して表示してもらえるように依頼した。

# ExcelファイルからHTML変換
Excelの編集は[EPPlus](https://www.epplussoftware.com/ja/)というExcel操作ライブラリーを使用している。
※EPPlus Ver 5系から商用利用は有償となっているため、Ver 4.5系(LGPL ライセンス)を使う。

EPPlusにはHTML変換する機能は備わっていないので、以前ExcelファイルからPDF変換に使用したSpireライブラリーを使用することにした。

https://qiita.com/yaju/items/cd54a0bf46b1bfec4a8f

## Spire.Office
中国の[E-iceblue社](https://www.e-iceblue.com/)が提供するofficeの.NETコンポーネントのSpire.Office があり、印刷およびPDF出力やHTML出力をサポートしている。さらに機能制限がある無償版FreeSpire.Officeも提供している。

Spire.officeには、下記コンポーネントが含まれている。個別にダウンロードすることも出来る。

|コンポーネント|内容|無償版制限|
|:--|:--|:--|
|Spire.Doc|Word ファイルの作成・読み・書き・変換・印刷・エクスポート|Wordファイルの編集は、500段落と25テーブルに制限されています。 \\ PDFファイルの変換は最初の3ページのみ。|
|Spire.DocViewer|Word ファイルの表示・変換| |
|Spire.XLS|Excel ファイルの新規作成・編集・変換|Excelファイルの編集は、ブックごとに5シート、1シートあたり200行に制限されています。|
|Spire.Presentation|PowerPont ファイルの新規作成・編集・変換|プレゼンテーションスライドが10枚に制限されています。|
|Spire.PDF|PDFドキュメントの作成・読み・書き・編集|PDFファイルの編集は、10ページに制限されています。|
|Spire.PDFViewer|PDFドキュメントをストリーム、ファイル、バイト配列から読み込み| |
|Spire.DataExport|Word/Excel/Access、html、PDF、DBF、SQLスクリプト、csv等の形式にデータをエクスポート| |
|Spire.Barcode|バーコードの生成| |

## 制限の確認
NuGetで「FreeSpire.XLS ver 12.2.0」をインストールする。
FreeSpire.XLSでは、Excelファイルの編集はブックごとに5シート、1シートあたり200行に制限されているのですが、Excelの編集自体はEPPlusを使用するので心配ない。問題はExcelファイルからHTML変換で1シートあたり200行に制限があるかを確認する。

### ソースコード
EPPlusで300行のExcelファイルを作成して、FreeSpire.XLSでHTML変換する。

```csharp
using OfficeOpenXml;
using OfficeOpenXml.Style;
using Spire.Xls;
using System.IO;
using ExcelHorizontalAlignment = OfficeOpenXml.Style.ExcelHorizontalAlignment;


const int START_ROW = 2;
const int COL_ID = 2;
const int COL_NAME = 3;
const int COL_BIRTHDAY = 4;

private void button1_Click(object sender, EventArgs e)
{
    string filePath = @"sample.xlsx";
    FileInfo excelFile = new FileInfo(filePath);
    if (excelFile.Exists)
    {
        excelFile.Delete();
        excelFile = new FileInfo(filePath);
    }

    using (var package = new ExcelPackage(excelFile))
    {
        ExcelWorksheet sheet = package.Workbook.Worksheets.Add("sheet");

        // ヘッダーにあたる行を作成
        CreateHeaderRow(sheet);

        // 300行のデータを作成
        Random rnd = new Random();
        foreach (var index in Enumerable.Range(1, 300))
            CreateRow(sheet, index, rnd);

        // 名前と誕生日の幅を広げる
        sheet.Column(COL_NAME).Width = 14;
        sheet.Column(COL_BIRTHDAY).Width = 16;

        //Excelのファイルをセーブする。
        package.Save();
    }

    // HTMLに変換
    SaveToHtml(filePath);

    MessageBox.Show("完了");
}

// ExcelからHTMLに変換する
private void SaveToHtml(string path)
{
    // Spire.XlsでExcelファイルを読み込む
    Workbook workbook = new Workbook();
    workbook.LoadFromFile(path);

    // ExcelからHTMLに変換
    Worksheet sheet = workbook.Worksheets[0];
    sheet.SaveToHtml("sample.html");
}

// ヘッダー行を作成する
private void CreateHeaderRow(ExcelWorksheet sheet)
{
    sheet.Cells[START_ROW, COL_ID].Value = "ID";
    sheet.Cells[START_ROW, COL_NAME].Value = "名前";
    sheet.Cells[START_ROW, COL_BIRTHDAY].Value = "誕生日";

    // ヘッダーセルにスタイルを適用する
    var headerCells = sheet.Cells[START_ROW, COL_ID, START_ROW, COL_BIRTHDAY];

    // 4方に罫線
    headerCells.Style.Border.Top.Style = ExcelBorderStyle.Thin;
    headerCells.Style.Border.Left.Style = ExcelBorderStyle.Thin;
    headerCells.Style.Border.Bottom.Style = ExcelBorderStyle.Thin;
    headerCells.Style.Border.Right.Style = ExcelBorderStyle.Thin;
    // 薄いグリーンの背景色で塗りつぶす
    headerCells.Style.Fill.PatternType = ExcelFillStyle.Solid;
    headerCells.Style.Fill.BackgroundColor.SetColor(Color.LightGreen);
    // テキストはセンタリング
    headerCells.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center;
    // 太字
    headerCells.Style.Font.Bold = true;
}

// index行目のデータを作る
private void CreateRow(ExcelWorksheet sheet, int index, Random rnd)
{
    int row = START_ROW + index;

    // id列を作る
    sheet.Cells[row, COL_ID].Value = index;
    // 名前も適当
    sheet.Cells[row, COL_NAME].Value = "鈴木　太郎" + index;
    // 誕生日も適当
    int year = DateTime.Now.AddYears(-rnd.Next(50)).Year;
    int month = rnd.Next(12) + 1;
    int day = rnd.Next(28) + 1;
    sheet.Cells[row, COL_BIRTHDAY].Value = new DateTime(year, month, day);
    // yyyy年mm月dd日形式で誕生日を表示
    sheet.Cells[row, COL_BIRTHDAY].Style.Numberformat.Format = "yyyy年mm月dd日";

    // 全ての列に4方に罫線のあるスタイルを作って適用する
    var cells = sheet.Cells[row, COL_ID, row, COL_BIRTHDAY];
    cells.Style.Border.Top.Style = ExcelBorderStyle.Thin;
    cells.Style.Border.Left.Style = ExcelBorderStyle.Thin;
    cells.Style.Border.Bottom.Style = ExcelBorderStyle.Thin;
    cells.Style.Border.Right.Style = ExcelBorderStyle.Thin;
}
```

### 出力結果
ExcelファイルからHTML変換で300行表示出来たので、200行の制限については問題なさそうだ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/5ca64221-fc9e-f870-e1c1-858ef37558dd.png)

下記サイトを参考に小計を計算するためにFormulaプロパティに式を使用したものをHTML変換すると例外エラーが発生したので、あくまで簡易なExcelファイルの変換で使用すると割り切った方がいいのかも。

https://chawatoyo.blog.fc2.com/blog-entry-2.html

【2022/02/21追記】
例外エラーは「System.NullReferenceException: 'オブジェクト参照がオブジェクト インスタンスに設定されていません。'」で、Formulaを設定しているところをコメントアウトすると例外エラーにならなくなる。
下記のSpire.XLSフォーラムを参考に変換時に数式を計算しないように「workbook.ConverterSetting.IsReCalculateOnConvert = false;」を追加したけど結果は変わらずれ例外エラーのまま。
[Export to Pdf Value of =SUM Field is wrong - Forum / Spire.XLS](https://www.e-iceblue.com/forum/export-to-pdf-value-of-sum-field-is-wrong-t10868.html?sid=e65f510722d055073acc00b213cb58b1)

PDF変換「sheet.SaveToPdf("sample.pdf");」に切り替えるとFormulaを設定した状態でも例外エラーなく、小計も計算されてPDF変換される。

# BoostrapのModalで別ページを表示する
BoostrapのModalにて、iframeを使用して別ページを表示する。

* [bootstrap modal iframe - programador clic](https://programmerclick.com/article/63011566056/)
* [Bootstrap 4 のmodalの表示幅を画面いっぱいにするには](https://www.e-pokke.com/blog/bootstrap4-modal-width.html)
* [【JavaScript】ブラウザの高さを取得して要素のheightに設定する](https://into-the-program.com/window-height-elem/)

```html:Test.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css">
    <style type="text/css">
      .modal-dialog-fluid {
         max-width: inherit;
         width: 98%;
         margin-left: 15px;
      }
    </style>
    <script type="text/javascript">
        window.addEventListener("load", () => {
            let elem = document.getElementById("content");
            let wh = window.innerHeight - 250;
            elem.style.height = wh + "px";
        }, false);
    </script>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.bundle.min.js"></script>
</head>
<body>
<button class="btn btn-primary" data-toggle="modal" data-target="#myModal">Trigger Modal in iFrame</button>
 
<!-- Modal -->
<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
    <div class="modal-dialog modal-dialog-fluid" role="document">
        <div class="modal-content">
            <div class="modal-header bg-info text-white">
                <h4 class="modal-title" id="myModalLabel">動作デモ</h4>
                <button type="button" class="close" data-dismiss="modal" aria-hidden="true" aria-label="Close">
                    <span aria-hidden="true">×</span>
                </button>
            </div>
            <div class="modal-body">
                <iframe src="sample.html" width="100%" id="content" frameborder="0" allowtransparency="true"></iframe>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-dismiss="modal">閉じる</button>
            </div>
        </div>
    </div>
</div>
</body>
</html>
```

## 表示結果
テストでは「Trigger Modal in iFrame」ボタンをクリックすると直ぐにModal画面で別ページを表示している。
実際にはクリックしたら抽出条件などをPostAJaxを用いて送信して、ASP.NETのサーバー上でExcelファイルの生成およびHTML変換が完了した段階でModal画面を表示する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/ab776b4c-bfb9-6922-7328-24fdd4122777.png)
クリック後にModal画面で別ページの`sample.html`が表示される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/cb3e6a15-11f2-30c4-7de2-761d5404e61f.png)

## iFrameのソースを変更したい
Boostrapでモーダルを開く場合、Clickイベントの記述がなくてもクリックすると`data-toggle`と`data-target`の属性指定によりモーダル表示されるようになっています。
iFrameのソースを条件よって変更したい場合、モーダルを開く前に処理を行う必要があります。Boostrapに則った方法を調べると`show.bs.modal`を使用する方法がありました。

https://v4.bootstrap-guide.com/javascript/modals/events

下記の方法でモーダルが開く前にiFrameのソースを変更することが出来ます。
条件によってモーダルを開きたくない場合、`return false`でキャンセルすることができます。

```js
$(document).on('show.bs.modal','.modal', function () {
     // キャンセルしたい場合、return false;
     $("#myModal iframe").attr("src", "sample.html");
})
```
本当は「$('#myModal').on('show.bs.modal', function ()」のように記載したかったんですが、何故か動かなくて下記参照サイトのようにしたら動きました。
[bootstrap jquery show.bs.modal event won't fire](https://stackoverflow.com/questions/19279629/bootstrap-jquery-show-bs-modal-event-wont-fire)

# 最後に
これでOfficeがインストールされていないPCでも、見た目がExcelのような表示を実現できた。
Officeがインストールされている環境があれば、Excelファイルを開けるボタンを用意するなどすればいいでしょう。

式などあるならHTML変換よりPDF変換の方がいいかも知れない、PDFビューアーすらインストールされていない場合は`PDF.js`を使用して表示させる。