[ASP.NET Core][ClosedXML] 셀 값에서 주소 가져오기 및 다시 쓰기

내가 하고 싶은 일은?


  • 클라이언트측에서 Excel 파일(.xlsx 또는 .xlsm) 업데이트
  • 셀 값으로 시트 가져오기 및 셀 찾기
  • 기반 셀에 텍스트 다시 쓰기
  • 기본 셀 값은 "A01", "B02"등과 같습니다
  • .
  • 셀 값의 주소가 잘 정의되지 않음
  • 파일이 인쇄 영역 및 페이지 나누기로 설정됨
  • 모든 대상 셀이 첫 페이지에 있음
  • aving을 파일로 작성하고 다운로드한 후

  • 환경


  • .NET 버전 6.0.201
  • ClosedXML ver.0.95.4

  • 샘플



    기본 프로젝트



    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;
        }
    }
    


    파일 올리기





    검색 셀 영역 가져오기



    파일을 불러오기 전에 검색 셀 영역을 결정할 수 없기 때문입니다.
    그래서 페이지 나누기, 인쇄 영역 또는 사용된 셀에서 영역을 가져옵니다.

    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));
        }
    ...
    


  • Print Areas and Page Breaks - ClosedXML/ClosedXML Wiki - ClosedXML - GitHub

  • 해당 값에서 셀 검색



    재작성 대상 셀을 검색하려면 어떻게 해야 합니까?
    모든 셀 값으로 검색할 수 있습니다.

    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);  
            }
        }
    ...
    


    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);
            }
        }
    ...
    


    이 샘플에서 실행 시간에는 차이가 없습니다.

    값을 다시 쓴 후 셀 스타일이 변경되지 않았습니다.

    결과



    좋은 웹페이지 즐겨찾기