以前の記事
Docker + .NET環境を作る
今回、作成する見積書は以下の通りです。
まずは、ASP.NET環境を作ります。VSCode+Remote - Containers拡張を使って作ります。より詳しくは、連載第1回を参照してください。Remote - Containers拡張がインストールされている状態で、次の手順を実行し、まずは、Docker+.NET環境を作ります。
【手順】Docker+ .NET環境を作る
[1]適当なワークフォルダを作ってVS Codeで開く
適当なフォルダを作ります。例えば、個人フォルダ以下に、「exampleaspnet」というフォルダを作ります。そして、そのフォルダをVSCodeで開きます。
[2]Dockerfileを書く
このフォルダの直下に、コンテナを起動するためのDockerfileを書きます(リスト1)。
FROM mcr.microsoft.com/dotnet/sdk:6.0
[3]コンテナを起動する
[F1]キーを押してコマンドパレットを開き、[Remote - Containers Reopen in Container]を選択し、[From 'Dockerfile']を選択します。すると、.NET環境が含まれたDockerコンテナが起動します。
[4]ターミナルを起動する
[Terminal]メニューから[New Terminal]を起動して、ターミナルを起動します。Dockerコンテナが起動していれば、このターミナルは、WindowsではなくDockerコンテナ内のターミナルとなり、コマンドプロンプトは、次のようになるはずです(XXXXXXXXXXXXはランダムな文字列)。以下、コマンド入力の場面では、このターミナルに入力してください。
root@XXXXXXXXXXXXX:/workspaces/exampleaspnet#
ASP.NETのプロジェクトを作る
今回は、帳票の作り方を説明するのが目的なので、ASP.NET自体の解説は極力避け、自動生成できるものは使う方針で進めていきます。まずは、ターミナルから次のように入力して、ASP.NET Razor Pagesプロジェクトを作ります。引数に指定した「exampleapp」フォルダができ、そのなかにASP.NET Razor Pagesのファイル一式が作られます。
dotnet new webapp -o exampleapp
動作確認する
サンプルページが作られているので、ビルドして実行して動作確認しましょう。まずは、いま作られたexampleappにカレントフォルダを移動します。
cd exampleapp
そのうえで、次の手順でビルドおよび実行します。
【手順】ビルドおよび実行する
[1]ビルドする
次のコマンドを入力してビルドします。ビルドすると、引数に指定しているpublishedディレクトリにexampleapp.dllファイルが作られます。
dotnet publish -c Release -o published
[2]実行する
次のコマンドを入力して実行します。実行すると、ASP.NETのサーバーが起動して待ち受け状態になります。
dotnet published/exampleapp.dll
[3]ブラウザを開く
VSCodeの右下に、図2の画面が表示されます。[Open in Browser]ボタンをクリックすると、ブラウザが開き、図3のように作られたASP.NETのサンプルページが表示されます。なお、図2の画面を開かなくとも、ブラウザから「http://localhost:5000/」と入力すれば開けます。動作の確認が終わったら、[Ctrl]+[C]キーを押して、停止してください。
Remote Containers拡張のポートフォワード機能
この手順のように「http://localhost:5000/」で開けるのは、Remote Containers拡張のおかげです。通常、コンテナのネットワークは独立しているため、「自分(localhost)のポート」と「コンテナ内のポート」を明示的に転送する設定をしないと通信できません(Dockerに詳しい人向けに説明すると、dockerコマンドの-pオプションによるポート番号の対応付けが必要です)。
しかし、Remote Containersでコンテナを起動した場合は、コンテナ内でプロキシが実行され、ポートの転送ができるようになっています。図2の画面で[See all forwarded Port]をクリックすると、[Ports]タブが開き、どのポートが転送設定されているのかを確認したり、追加・変更したりできます(図4)。
サンプルデータベースの作成
さて、この図3のプログラムをベースに、冒頭の図1に示した帳票を出力するサンプルを組み込んでいきます。簡単に済ませるとすれば、「何かボタンをクリックすると、いつも決まった帳票が出力される」というサンプルを作ることですが、帳票出力といえば、「データベースに格納されたデータを取り出して帳票を作りたい」ということが、ほとんどではないでしょうか。
ここでは実用に寄せて、簡単なデータベースを作り、そこに格納されているデータを帳票出力するというサンプルを作ります。とはいえきちんとしたデータベースやページを作るとたいへんですし、それが本連載の目的ではないので、できるだけ簡素化します。
ここでは、マイクロソフト社の下記のページにある手順に則り、「モデルのクラスを作り、そこからデータベース、テーブル、そして、初期ページの自動生成」までを実施します。下記の工程では、サンプルとして不必要なページが作られますが、そのあたりは目をつむります。話を簡単にするため、データベースには、事前にインストールや初期設定をしておく必要がないSQLiteを使います。
【手順】サンプルデータベースの作成
[1]EF Coreツールなどのインストール
今回は、データベース操作に「Entity Framework(EF) Core」を使っていきます。そのためのツールやライブラリをインストールするため、次のコマンドを入力します。
dotnet tool install -g dotnet-aspnet-codegenerator dotnet tool install --global dotnet-ef dotnet add package Microsoft.EntityFrameworkCore.Design dotnet add package Microsoft.EntityFrameworkCore.SQLite dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
ツールは、ホームディレクトリの/.net/toolsフォルダにインストールされます。次のように入力して、パスを通しておきます。
export PATH="$PATH:/$HOME/.dotnet/tools"
[2]モデルを作る
ツールの準備が整ったら、まずは、テーブルの基となる「モデル」を作ります。ここでは、「注文(Order)」と「明細(Detail)」の親子関係を持つテーブルとします。exampleappフォルダの下にModelsフォルダを作り、その下にリスト2の内容で、Models.csファイルを作ります。
using System; using System.ComponentModel.DataAnnotations; namespace exampleapp.Models { public class Order { public int OrderID { get; set; } public string Zip { get; set;} public string Address1 {get; set;} public string Address2 {get; set;} public string Company {get; set;} public string Person {get; set;} public string Email {get; set;} [DataType(DataType.Date)] public DateTime OrderDate { get; set; } public ICollection<Detail> Details {get; set;} } public class Detail { public int ID { get; set; } public int OrderID {get; set;} public string ProductName { get; set;} public decimal UnitPrice {get; set;} public int Quantity {get; set;} } }
EF Coreで扱う場合、プロパティに命名規則があります。ここでは、次の命名規則に従っています。
- 主キー:「ID」や「クラス名+ID」を付けると、それは主キーとして扱われます(「OrderID列」および「ID列」)。
- リレーション:親子関係は、「ICollection<クラス名>」で構成します(OrderとDetailの関係)。子が親を参照する外部キーは、「クラス名+ID」で命名します(DetailクラスのOrderIDプロパティ)。
[3]ページを自動生成する
このモデル定義から、データベースへの接続定義や一覧や編集、削除のページなど、必要なコード一式を自動で生成します。
まずは、Orderテーブルについて、次のようにして作成します(ここでは「Pages/Orders」のように、-outDirオプションを「Orders」としている点に注意してください。Ordersである必要はないのですが、「Order」のようにモデルのクラス名と同じにすると「ページ名」と「モデルのクラス名」が同名になり、ビルド時にエラーが発生します)。
dotnet-aspnet-codegenerator razorpage -m Order -dc MyAppContext -udl -outDir Pages/Orders --referenceScriptLibraries -sqlite
同様にして、Detailテーブルについて作成します。
$HOME/.dotnet/tools/dotnet-aspnet-codegenerator razorpage -m Detail -dc MyAppContext
[4]データベースやテーブルを作る
これでPagesフォルダ以下に、「Orders」と「Details」のフォルダができ「一覧(Index)」「新規作成(Create)」「編集(Edit)」「削除(Delete)」の4つのページが作成されます。しかしデータベースもテーブルもまだないので、実行して開いてもエラーが発生するだけです。そこでデータベースとテーブルを作成していきましょう。
EF Coreには、このモデルから、自動でデータベースやテーブル自体を作成するマイグレーション機能があります。まず、次のように入力します。
dotnet ef migrations add InitialCreate
するとMigrationディレクトリ以下に、データベースやテーブルを作成するためのプログラムが自動生成されます。これをデータベースに反映するため、下記のコマンドを入力します。このコマンド入力によって、実際にデータベースとテーブルが作成されます。
dotnet ef database update
[5]動作確認する
この段階で動作確認しておきます。ビルドして起動します。
dotnet publish -c Release -o published
dotnet published/exampleapp.dll
ブラウザで、「http://localhost:5000/Orders/」を開きます。すると、Orderテーブルの一覧が表示されます(図5)。ここで[Create New]のリンクをクリックすると、新しい注文を作成できますが、何もしないでおいてください(以下の手順では、「何もレコードが存在しないとき」に限って、初期データを作成するプログラムを作るため、何かデータを入れると次の手順で失敗します)。
初期データの投入
まだテーブルの中身が空なので、サンプルのデータを投入します。いくつかのやり方がありますが、ここでは「何もレコードが存在しないとき」に限って、初期データを作成するプログラムを作ります。
あまり手間をかけたくないので、Program.csをリスト3のように書き換えます。書き換えた箇所には「★印」を付けました。
using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; // ★追加★ using exampleapp.Models; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddRazorPages(); builder.Services.AddDbContext<MyAppContext>(options => options.UseSqlite(builder.Configuration.GetConnectionString("MyAppContext"))); var app = builder.Build(); // ★初期化データ投入 ここから★ using (var scope = app.Services.CreateScope()){ var services = scope.ServiceProvider; using (var context = services.GetRequiredService<MyAppContext>()) { if (!await context.Order.AnyAsync()) { var order = new Order { Zip = "123-4567", Address1 = "東京都新宿区", Address2 = "1-2-3 ●●ビル", Company = "株式会社●●", Person = "山田一郎", Email = "yamada@example.co.jp", OrderDate = DateTime.Today, Details = new List<Detail> { new Detail{ ProductName = "製品A", UnitPrice = 100, Quantity = 5}, new Detail{ ProductName = "製品B", UnitPrice = 300, Quantity = 3}, new Detail{ ProductName = "製品C", UnitPrice = 400, Quantity = 1}, } }; context.Order.Add(order); await context.SaveChangesAsync(); } } } // ★追加ここまで★ // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.MapRazorPages(); app.Run();
このように修正してビルドして実行し、先ほどと同様に「http://localhost:5000/Orders/」を確認すると、初期データが入っていることがわかります(図6)。図6には表示されていませんが、実際には明細も入っています。
帳票ページを作る
前置きが長くなりましたが、ここから帳票の作成開始です。ここでは、「http/localhost:5000/Orders/ChouHyou?id=IDの値」というURLで呼び出されたとき、そのIDを持つ注文の見積書をPDFでダウンロードできるようにするようにしていきたいと思います。
DioDocs for Excelのインストール
帳票の作成では、DioDocs for Excelを使います。第2回の手順と同様にして、NuGetパッケージマネージャを使って、インストールしておいてください。
帳票の基となるExcelテンプレートの準備
まずは、帳票の基となるExcelテンプレートを準備します。グレープシティ社のDioDocs for Excelでは、「既存のExcelファイルを読み込んで、そこに値を入れて出力する」ことができるため、このテンプレートは、Excelで自在に見栄えのよいものが作れます。ここでは、図7のように用意しました。見栄えのよいものが作れるという利点を活かし、見出しのところには色を付けてみました。
図7に用意したテンプレートでは、「小計」「消費税」「合計」などをあらかじめExcelの計算式として設定しているという点に注目してください。「金銭型」の指定もしています。
また小計は空欄のときに「¥0」と表示されないよう、カスタム書式で「¥#,##0_);[赤](¥#,##0);
」を指定しています。なお、利用するフォントはDockerコンテナにすべて含めなければならない点に注意してください。
前回説明したように、例えばAdobe社がオープンソースとして提供している「源ノ角ゴシック(source-han-sans)」や「源ノ明朝(source-han-serif)」などを使って作ります。これらの書体は、いくつかの「太さ」(ウェイト)のものが用意されているため、「御見積書」などの文字は太いウェイトのものを使うと、美しい見栄えにできます。
こうして用意したExcelテンプレートをDockerコンテナのなかに置きます。ここでは、ExcelTemplateというフォルダを作り、そのなかに、chouhyou.xlsxというファイル名として置きました。そしてフォントも必要です。fontsフォルダのなかに、利用したフォント一式を入れました(図8)。
帳票を生成するプログラム
以上で準備が整いました。帳票を作成するプログラムを作ります。Pages/Ordersフォルダに「ChouHyou.cshtml.cs」というファイルを作ります(リスト4)。また「ChouHyou.cshtml」というファイルも作ります(リスト5)。
#nullable disable using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.EntityFrameworkCore; using System.Globalization; // DioDocs for Excelの読み込み using GrapeCity.Documents.Excel; // モデルの読み込み using exampleapp.Models; namespace exampleapp.Pages.Orders { public class ChouHyouModel : PageModel { private readonly MyAppContext _context; public ChouHyouModel(MyAppContext context) { _context = context; } [BindProperty] public exampleapp.Models.Order Order { get; set; } public async Task<IActionResult> OnGetAsync(int? id) { if (id == null) { return NotFound(); } // 明細混みでデータベースから読み込む Order = await _context.Order.Include(c => c.Details) .FirstOrDefaultAsync(m => m.OrderID == id); if (Order == null) { return NotFound(); } // 帳票処理 // 新規ワークブックの作成 var workbook = new GrapeCity.Documents.Excel.Workbook(); // テンプレートを開く workbook.Open("ExcelTemplate/chouhyou.xlsx"); // フォントとカルチャーの設定 Workbook.FontsFolderPath = "fonts"; workbook.Styles["標準"].Font.Name = "SourceHanSerif-Regular"; workbook.Culture = CultureInfo.GetCultureInfo("ja-JP"); // データの埋め込み IWorksheet worksheet = workbook.ActiveSheet; // 見積日 worksheet.Range["D2"].Value = DateTime.Today; // 取引先の会社名 worksheet.Range["A4"].Value = Order.Company + " 御中"; // 明細 int row = 11; foreach (Detail detail in Order.Details) { worksheet.Cells[row, 0].Value = detail.ProductName; worksheet.Cells[row, 1].Value = detail.Quantity; worksheet.Cells[row, 2].Value = detail.UnitPrice; row = row + 1; } worksheet.Cells[row, 0].Value = "* 以下余白 *"; // PDF化 var stream = new MemoryStream(); workbook.Save(stream, SaveFileFormat.Pdf); return File(stream.GetBuffer(), "application/pdf", "chouhyou.pdf"); } } }
@page @model exampleapp.Pages.Orders.ChouHyouModel
データベースのデータは、次のようにして読み込めます。明細も含めて読み込むため、Includeを指定しています。
// 明細混みでデータベースから読み込む Order = await _context.Order.Include(c => c.Details) .FirstOrDefaultAsync(m => m.OrderID == id);
こうしてデータベースから読み込んだOrderを帳票化します。
既存のExcelファイルの読み込み
まずは、Excelワークブックを作り、用意しておいたテンプレートのExcelファイルを読み込みます。
// 新規ワークブックの作成 var workbook = new GrapeCity.Documents.Excel.Workbook(); // テンプレートを開く workbook.Open("ExcelTemplate/chouhyou.xlsx");
フォントとカルチャーの設定
そして、フォントとカルチャーを設定します。フォントは文字化けしないようにするために必須なのは言うまでもありませんが、カルチャーも大事です。この設定を忘れると、日付が「年/月/日」ではなくて「月/日/年」のように表示されてしまいます。
// フォントとカルチャーの設定 Workbook.FontsFolderPath = "fonts"; workbook.Styles["標準"].Font.Name = "SourceHanSerif-Regular"; workbook.Culture = CultureInfo.GetCultureInfo("ja-JP");
値の埋め込み
次に、値を埋め込んでいきます。まずは、ワークシートを取得します。
IWorksheet worksheet = workbook.ActiveSheet;
そして値を埋め込みます。特定のセルに値を設定するには、次のように、Rangeプロパティを使えばよいでしょう。
// 見積日 worksheet.Range["D2"].Value = DateTime.Today;
ループなどで回したいときは、Cellsプロパティを使って、「行」「列」の番号を指定するとよいでしょう。行・列は、どちらも0始まり(0オリジン)です。
// 明細 int row = 11; foreach (Detail detail in Order.Details) { worksheet.Cells[row, 0].Value = detail.ProductName; worksheet.Cells[row, 1].Value = detail.Quantity; worksheet.Cells[row, 2].Value = detail.UnitPrice; row = row + 1; }
PDFに変換して返す
PDFに変換してダウンロードさせるには、いくつかの方法がありますが、MemoryStreamオブジェクトを作って、そこに書き出して、そのバッファを出力するのが簡単です。引数に指定している「application/pdf」はコンテンツタイプ「chouhou.pdf」は、ダウンロードするファイルに付ける既定のファイル名です。
// PDF化 var stream = new MemoryStream(); workbook.Save(stream, SaveFileFormat.Pdf); return File(stream.GetBuffer(), "application/pdf", "chouhyou.pdf");
帳票ダウンロードへのリンクを貼る
以上でプログラムは完成です。最後に、Orders/Index.cshtmlに、帳票ダウンロードへのリンクを貼ります。
<td> <a asp-page="./Edit" asp-route-id="@item.OrderID">Edit</a> | <a asp-page="./Details" asp-route-id="@item.OrderID">Details</a> | <a asp-page="./Delete" asp-route-id="@item.OrderID">Delete</a> </td>
もともとのIndex.cshtmlの部分に、ChouHyouへのリンクを加えるだけです。
<td> <a asp-page="./Edit" asp-route-id="@item.OrderID">Edit</a> | <a asp-page="./Details" asp-route-id="@item.OrderID">Details</a> | <a asp-page="./Delete" asp-route-id="@item.OrderID">Delete</a> <!-- 帳票へのリンクを追加 --> <a asp-page="./ChouHyou" asp-route-id="@item.OrderID">帳票ダウンロード</a> </td>
これで完成です。実行してhttp://localhost:5000/Orders/にアクセスすると「帳票ダウンロード」というリンクが表示されており、このリンクをクリックすると、その帳票がPDF形式でダウンロードできます(図9)。ダウンロードできる帳票は、図10の通りです。
まとめ
本連載では、Docker環境でDioDocs for Excelを使う方法を説明してきました。Dockerをそのまま開発に使うと少しややこしいのですが、Remote - Containers拡張を使えばそうした複雑さがなく開発できます。
Dockerは、自分のホストとは隔離された環境なので、「フォント」や「日本語設定」が引き継がれません。なのでフォントとカルチャーの設定し忘れには注意しましょう。
今回お見せしたように、DioDocs for ExcelはExcelでテンプレートを作っておき、そこに値を埋め込んで出力することができるため、見栄えのよいデザインの帳票が作りやすいのが魅力です。罫線や飾り、グラフィックなどを埋め込むことが容易なため、帳票に限らず商品ラベルや宛名ラベルなど、さまざまな「差し込み印刷」を実現したい場面でも重宝すると思います。