C#でExcelテンプレートファイルを基にエクスポート機能を作成する方法-NPOI 


:::note info
<a href="https://github.com/nissl-lab/npoi">NPOI</a>を採用するケースです。
:::

### コード例

```csharp
public ActionResult ExcelExp()
{
    ExcelExporter excelExporter = new ExcelExporter();

    // エクスポート処理
    using (var stream = new MemoryStream())
    {//※ストリームが自動的に閉じられ、リソースが解放されるため、手動で Close や Dispose を呼び出す必要はなし
  
        // テンプレートに基づいてエクスポート内容を作成
        string templatePath = Server.MapPath(ConfigurationManager.AppSettings["ExpTemp"]);
        excelExporter.ExportToStream(stream, templatePath);

        // MemoryStreamをバイト配列に変換  
        byte[] fileBytes = stream.ToArray();

        // ダウンロード用にファイルを返す
        return File(fileBytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "exportedFile.xlsx");    
    }
}

public class ExcelExporter
{
    public void ExportToStream(MemoryStream stream, string templatePath)
    {
        // テンプレートファイルを読み込む
        using (var fileStream = new FileStream(templatePath, FileMode.Open, FileAccess.Read))
        {
            IWorkbook workbook = new XSSFWorkbook(fileStream);
            ISheet sheet = workbook.GetSheetAt(0);

            // セルにデータを挿入
            IRow row = sheet.GetRow(1) ?? sheet.CreateRow(1);
            ICell cell = row.GetCell(1) ?? row.CreateCell(1);
            cell.SetCellValue("Sample Data");

            // スタイルのカスタマイズ
            ICellStyle style = workbook.CreateCellStyle();
            IFont font = workbook.CreateFont();
            font.FontHeightInPoints = 12;
            font.FontName = "Arial";
            font.IsBold = true;
            style.SetFont(font);
            style.FillForegroundColor = IndexedColors.LightYellow.Index;
            style.FillPattern = FillPattern.SolidForeground;
            cell.CellStyle = style;

            // メモリストリームに書き込み
            workbook.Write(stream, true);//★★★★★
            // ストリームの位置を先頭に戻す
            stream.Position = 0;//★★★★★
        }
    }
}
```

### ポイント

- **`★★★★★` は要注意**:
  -workbook.Writeの実行にstreamを自動的に閉じないようにしておいてください

:::note warn
シートにあるグラフに対して、動的にデータ範囲を更新する処理はNPOIがうまくいけないようです。
:::

:::note warn
NPOIを使用している際に、結合セルが解除される現象は比較的よく報告される問題です。原因としては、NPOIがExcelテンプレートの結合セルを読み込んだ後に、明示的に再設定しないと結合セルの情報が失われる場合があるためです。

以下の方法で対処できます。

---

### **結合セルを再設定する方法**

1. **結合セルを再度設定するコード**
   テンプレートを使用する場合でも、結合セルを再設定する必要があります。以下のコードを使用して、必要なセルを再結合します。

   ```csharp
   using NPOI.SS.UserModel;
   using NPOI.XSSF.UserModel;

   public void 再結合を設定(IWorkbook workbook, ISheet sheet)
   {
       // 必要な結合範囲を明示的に設定
       sheet.AddMergedRegion(new NPOI.SS.Util.CellRangeAddress(
           2, 2, // 開始行と終了行（0ベース）
           3, 4  // 開始列と終了列（0ベース）
       ));
   }
   ```

   **呼び出し例:**
   ```csharp
   IWorkbook workbook = new XSSFWorkbook("テンプレート.xlsx");
   ISheet sheet = workbook.GetSheetAt(0);

   // 結合セルを再設定
   再結合を設定(workbook, sheet);

   using (var fs = new FileStream("更新後.xlsx", FileMode.Create, FileAccess.Write))
   {
       workbook.Write(fs);
   }
   ```

---

2. **テンプレートの結合情報を保持する方法**
   テンプレートファイルの結合情報を動的に操作しない場合、読み取った結合情報を一度保存し、再設定することで解決できます。

   ```csharp
   public void 再設定結合セル(IWorkbook workbook, ISheet sheet)
   {
       // テンプレートから結合情報を取得
       var mergedRegions = new List<NPOI.SS.Util.CellRangeAddress>();
       for (int i = 0; i < sheet.NumMergedRegions; i++)
       {
           mergedRegions.Add(sheet.GetMergedRegion(i));
       }

       // セルを書き換える（ここで解除される場合がある）
       // ...

       // 結合セルを再設定
       sheet.RemoveMergedRegions(mergedRegions); // 既存の結合を解除
       foreach (var region in mergedRegions)
       {
           sheet.AddMergedRegion(region); // 元の結合範囲を再設定
       }
   }
   ```

---

3. **結合解除を回避するヒント**
   - **テンプレート編集を最小限にする:** NPOIでテンプレートを利用する際は、結合セルを含む部分を極力直接編集せず、他の部分を操作する方法を検討してください。
   - **NPOIの最新バージョンを使用する:** NPOIのバージョンによって動作が異なる場合があります。最新版にアップデートして、既知の問題が修正されているか確認してください。
   - **Alternate Libraries:** NPOIが問題を起こしやすい場合、ClosedXMLなどの代替ライブラリも検討してください（特に結合セルの操作がシンプルです）。

---

### **補足**
- テンプレート内のセル結合が破壊される理由として、NPOIでセルの書き換えやスタイル変更が行われたとき、結合情報が初期化されることが挙げられます。そのため、結合情報を再設定することが必要です。
- 大量の結合セルがある場合、結合情報の操作を効率化する工夫が必要です。たとえば、結合情報を事前にJSONやXMLとして保存しておき、再設定時に一括処理する方法も有効です。
:::
:::note alert
NPOIでは、既存の結合セル範囲と重なる新しい結合セルを追加することが許されていないため
このエラーは、同じ範囲を結合セルとして再設定しようとした際に発生します。NPOIでは、既存の結合セル範囲と重なる新しい結合セルを追加することが許されていないためです。

---

### **原因**
- `AddMergedRegion` を呼び出す際に、すでに同じ範囲が結合セルとして設定されている場合、NPOIは重複エラーをスローします。

---

### **解決方法**
エラーを回避するために、次の手順を実行します。

1. **結合セルを追加する前に、既存の結合範囲を確認する**
   - `sheet.IsMergedRegion(range)` を利用して、対象の結合範囲がすでに存在するかをチェックします。

2. **重複がある場合、既存の結合範囲を一度削除する**
   - `sheet.RemoveMergedRegion(index)` を使用して、既存の結合範囲を削除してから再設定します。

---

### **修正版コード例**

以下は、結合セルを安全に追加するコード例です。

```csharp
public void 安全に結合セルを追加(ISheet sheet, int firstRow, int lastRow, int firstCol, int lastCol)
{
    // 結合範囲を作成
    var newRange = new NPOI.SS.Util.CellRangeAddress(firstRow, lastRow, firstCol, lastCol);

    // 既存の結合範囲と重複するかチェック
    for (int i = 0; i < sheet.NumMergedRegions; i++)
    {
        var existingRange = sheet.GetMergedRegion(i);

        // 重複がある場合、削除
        if (existingRange.FirstRow == newRange.FirstRow &&
            existingRange.LastRow == newRange.LastRow &&
            existingRange.FirstColumn == newRange.FirstColumn &&
            existingRange.LastColumn == newRange.LastColumn)
        {
            sheet.RemoveMergedRegion(i);
            break; // 削除後にループを抜ける
        }
    }

    // 結合セルを追加
    sheet.AddMergedRegion(newRange);
}
```

---

### **この関数の使用方法**

上記の関数を、結合セルの再設定箇所で使用します。

```csharp
// 例: B7:B8 を結合する
安全に結合セルを追加(sheet, 6, 7, 1, 1); // 0-based index
```

---

### **解説**
1. **新しい範囲を作成**  
   `CellRangeAddress` で結合する範囲を指定します。

2. **既存の範囲と比較**  
   `sheet.GetMergedRegion(i)` で既存の結合範囲を取得し、新しい範囲と比較します。

3. **既存範囲を削除**  
   重複している場合に削除します。これにより、既存の範囲をクリアしてから新しい範囲を安全に追加できます。

4. **結合セルを追加**  
   `sheet.AddMergedRegion()` で新しい範囲を設定します。

---

### **注意点**
- **結合セルのインデックスは 0-based** です。
  - 例えば、セル `B7:B8` の場合、行は `6` (7-1)、列は `1` (B=2列目-1) として指定します。
- **結合セルの重複削除**  
  既存の範囲を適切に削除しないとエラーが発生します。

---

このアプローチで、結合セルの範囲が既存範囲と重ならずに処理できるはずです！
:::