What I want to do?
- Updating Excel files(.xlsx or .xlsm) from client-side
- Getting sheets and finding cells by cell values
- Rewriting texts into founded cells
- Default cell values are like "A01", "B02", and so on
- The address of cell values aren't well defined
- The files are set print areas and page breaks
- All of the target cells are in the first page
- After writing aving as files and downloading them
Environments
- .NET ver.6.0.201
- ClosedXML ver.0.95.4
Samples
Base project
Index.cshtml
<input type="file" accept=".xlsx,.xlsm" id="send_file_input"> <button onclick="Page.sendFile()">Send File</button> <a id="download_target"></a> <script src="/js/main.page.js"></script> main.page.ts
export async function sendFile(): Promise<void> { const file = await getSelectedFile(); if(file == null) { return; } if(/(xlsx|xlsm)+$/.test(file.name) === false) { console.error("Only for xlsx or xlsm"); return; } const fileData = await getSelectedFileData(file); if(fileData == null) { return; } const formData = new FormData(); formData.append("file", new Blob([fileData])) const response = await fetch("/files", { method: "POST", headers: { "File-Name": file.name, "File-Type": file.type }, body: formData }); if(response.ok) { await handleResponse(response); } else { console.error(response.statusText); } } async function handleResponse(response: Response): Promise<void> { switch(response.headers.get("Content-Type")){ case "application/json": await handleResultAsJson(response); break; default: await handleResultAsFile(response); break; } } async function handleResultAsJson(response: Response): Promise<void>{ const json = await response.json(); const result = JSON.parse(JSON.stringify(json)); if(result?.succeeded != null && result.succeeded === true){ alert(result.errorMessage); return; } alert("Failed"); } async function handleResultAsFile(response: Response): Promise<void> { const target = document.getElementById("download_target") as HTMLAnchorElement; target.download = response.headers.get("File-Name") ?? "file"; target.href = window.URL.createObjectURL(await response.blob()); target.click(); } function getSelectedFile(): File|null { const fileInput = document.getElementById("send_file_input") as HTMLInputElement; const file = fileInput.files?.item(0); if(file == null) { console.error("File was null"); return null; } return file; } async function getSelectedFileData(file: File): Promise<Uint8Array|null> { const fileData = await file.arrayBuffer(); if(fileData == null) { console.error("Failed getting ArrayBuffer"); return null; } return new Uint8Array(fileData); } FileController.cs
using BookshelfSample.Apps; using BookshelfSample.Files; using Microsoft.AspNetCore.Mvc; namespace BookshelfSample.Controllers; public class FileController: Controller { private readonly ISpreadsheetUpdater spreadsheetUpdater; public FileController(ISpreadsheetUpdater spreadsheetUpdater) { this.spreadsheetUpdater = spreadsheetUpdater; } [HttpPost] [Route("files")] public async Task<IActionResult> WriteFile([FromForm] IFormFile? file) { if(file == null) { Response.Headers["Content-Type"] = "application/json"; return Json(ActionResultFactory.GetFailed("file was null")); } var fileName = Request.Headers["File-Name"]; if(string.IsNullOrEmpty(fileName)) { Response.Headers["Content-Type"] = "application/json"; return Json(ActionResultFactory.GetFailed("FileName was null")); } var contentType = Request.Headers["File-Type"]; if(string.IsNullOrEmpty(contentType)) { Response.Headers["Content-Type"] = "application/json"; return Json(ActionResultFactory.GetFailed("ContentType was null")); } var result = await this.spreadsheetUpdater.SearchAndWriteAsync(file, fileName, contentType); Response.Headers["File-Name"] = result.FileName; return File(result.FileData, result.ContentType, result.FileName); } } SpreadsheetUpdater.cs
using System.Text; using System.Text.Json; using ClosedXML.Excel; using BookshelfSample.Apps; namespace BookshelfSample.Files; public class SpreadsheetUpdater: ISpreadsheetUpdater { private record CellAddress(int Column, int Row); private record CellArea(CellAddress AreaFrom, CellAddress AreaTo); public async Task<DownloadFile> SearchAndWriteAsync(IFormFile file, string fileName, string contentType) { try { // load uploaded file data. using var memoryStream = new MemoryStream(); using(var stream = file.OpenReadStream()) { await stream.CopyToAsync(memoryStream); } using var saveStream = new MemoryStream(); using (var book = new XLWorkbook(memoryStream)) { foreach(var sheet in book.Worksheets) { // TODO: Search and rewrite cell values. } book.SaveAs(saveStream); } return new DownloadFile(fileName, saveStream.ToArray(), contentType); } catch(Exception ex) { return GenerateFailedFile("Something wrong"); } } private DownloadFile GenerateFailedFile(string errorMessage) { var failedResult = ActionResultFactory.GetFailed(errorMessage); var resultJsonData = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(failedResult)); return new DownloadFile("failed.json", resultJsonData, "application/json"); } } WriteValueSamples.cs
namespace BookshelfSample.Files; public static class WriteValueSamples { public static Dictionary<string, string> GetValues() { var values = new Dictionary<string, string>(); values.Add("A01", "SampleA"); values.Add("A02", "SampleB"); values.Add("A03", "SampleC"); values.Add("A04", "SampleD"); values.Add("A05", "SampleE"); values.Add("B01", "あ"); values.Add("B02", "い"); values.Add("B03", "う"); values.Add("B04", "え"); values.Add("B05", "お"); values.Add("C01", "00111"); values.Add("C02", "00222"); values.Add("C03", "00333"); values.Add("C04", "00444"); values.Add("C05", "00555"); values.Add("D01", "AA"); values.Add("D02", "BB"); values.Add("D03", "CC"); values.Add("D04", "DD"); values.Add("D05", "EE"); values.Add("E01", "!#"); values.Add("E02", "$%&"); values.Add("E03", "()"); values.Add("E04", "|=\\"); values.Add("E05", "]-^["); return values; } } Upload files
Get searching cells area
This is because the search cell area cannot be determined before loading the files.
So I get the areas from the page breaks, print areas, or used cells.
SpreadsheetUpdater.cs
... public async Task<DownloadFile> SearchAndWriteAsync(IFormFile file, string fileName, string contentType) { try { ... using var saveStream = new MemoryStream(); using (var book = new XLWorkbook(memoryStream)) { foreach(var sheet in book.Worksheets) { var searchArea = this.GetSearchAreas(sheet); // TODO: search and write values } book.SaveAs(saveStream); } return new DownloadFile(fileName, saveStream.ToArray(), contentType); } catch(Exception ex) { return GenerateFailedFile("Something wrong"); } } private CellArea GetSearchAreas(IXLWorksheet sheet) { var printArea = this.GetPrintAreaAddresses(sheet); var usedAreaFrom = sheet.FirstCellUsed().Address; var usedAreaTo = sheet.LastCellUsed().Address; if(printArea == null) { // if the file doesn't have print areas, the result will be created by CellUsed return new CellArea(AreaFrom: new CellAddress(usedAreaFrom.ColumnNumber, usedAreaFrom.RowNumber), new CellAddress(usedAreaTo.ColumnNumber, usedAreaTo.RowNumber)); } // get the smallest area from PrintArea(+ Page breaks) and CellUsed. var columnFrom = (printArea.AreaFrom.Column > usedAreaFrom.ColumnNumber)? printArea.AreaFrom.Column: usedAreaFrom.ColumnNumber; var columnTo = (printArea.AreaTo.Column > usedAreaTo.ColumnNumber)? usedAreaTo.ColumnNumber: printArea.AreaTo.Column; var rowFrom = (printArea.AreaFrom.Row > usedAreaFrom.RowNumber)? printArea.AreaFrom.Row: usedAreaFrom.RowNumber; var rowTo = (printArea.AreaTo.Row > usedAreaTo.RowNumber)? usedAreaTo.RowNumber: printArea.AreaTo.Row; return new CellArea(AreaFrom: new CellAddress(columnFrom, rowFrom), new CellAddress(columnTo, rowTo)); } private CellArea? GetPrintAreaAddresses(IXLWorksheet sheet) { var printArea = sheet.PageSetup.PrintAreas.FirstOrDefault(); if(printArea == null) { return null; } var columnTo = 1; var rowTo = 1; var cellTo = printArea.LastCell().Address; // I only can get the last cell of page breaks. var columnBreak = sheet.PageSetup.ColumnBreaks.FirstOrDefault(); if(columnBreak <= 0 || cellTo.ColumnNumber < columnBreak) { columnTo = cellTo.ColumnNumber; } else { columnTo = columnBreak; } var rowBreak = sheet.PageSetup.RowBreaks.FirstOrDefault(); if(rowBreak <= 1 || cellTo.RowNumber < rowBreak) { rowTo = cellTo.RowNumber; } else { rowTo = rowBreak; } var firstCell = printArea.FirstCell().Address; return new CellArea(AreaFrom: new CellAddress(firstCell.ColumnNumber, firstCell.RowNumber), new CellAddress(columnTo, rowTo)); } ... Search cells from their values
How can I search rewrite target cells?
I can search by all cell values.
SpreadsheetUpdater.cs
... public async Task<DownloadFile> SearchAndWriteAsync(IFormFile file, string fileName, string contentType) { try { ... using var saveStream = new MemoryStream(); using (var book = new XLWorkbook(memoryStream)) { foreach(var sheet in book.Worksheets) { var searchArea = this.GetSearchAreas(sheet); this.WriteValues(sheet, searchArea); } book.SaveAs(saveStream); } return new DownloadFile(fileName, saveStream.ToArray(), contentType); } catch(Exception ex) { return GenerateFailedFile("Something wrong"); } } ... private void WriteValues(IXLWorksheet sheet, CellArea area) { var values = WriteValueSamples.GetValues(); var searchArea = sheet.Range(area.AreaFrom.Row, area.AreaFrom.Column, area.AreaTo.Row, area.AreaTo.Column); // Search only for cells that have a value foreach(var cell in searchArea.CellsUsed()) { var key = cell.GetString(); var value = values.FirstOrDefault(v => v.Key == key); if(string.IsNullOrEmpty(value.Key)) { continue; } cell.SetValue<string>(value.Value); } } ... I also can use IXLRange.Search.
SpreadsheetUpdater.cs
... private void WriteValues(IXLWorksheet sheet, CellArea area) { var values = WriteValueSamples.GetValues(); var searchArea = sheet.Range(area.AreaFrom.Row, area.AreaFrom.Column, area.AreaTo.Row, area.AreaTo.Column); foreach(var v in values) { var target = searchArea.Search(v.Key).FirstOrDefault(); if(target == null) { continue; } target.SetValue<string>(v.Value); } } ... In this sample, their execution times doesn't have any differences.
After rewriting the values, cell styles haven't been changed.


Top comments (0)