I'm fairly new to creating Excel spreadsheets in C# and I'm looking for advice.
I've spent 2 or 3 days now looking through documentation and blogs etc but I cannot seem to find an answer to a common task.
I need to insert a text value into a specific worksheet. I cant easily post my code at the moment but it appears to be an issue with every example I've seen of the definition of a sheet, it always gets the first child.
I need to iterate through all sheets and dependant on the sheet name then go and insert a value.
I.e. If the sheet name = "testA" then write TestA, if the sheet name = "testB" then write "TestB".
Currently I can insert a value for the workbook but it inserts the same value for every sheet.
Sheets sheets = workbookPart.Workbook.GetFirstChild<Sheets>();
[Microsoft documentation] (https://learn.microsoft.com/en-us/office/open-xml/how-to-insert-text-into-a-cell-in-a-spreadsheet)
Please note I'm not just giving up on this I've just reached a bit of a wall with it and need some pointers.
Many Thanks, J
Namespaces I'm using:
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Spreadsheet;
private void btnExport_Click(object sender, EventArgs e)
{
//Using Microsofts Interop class to create Excel files gives mixed resuts and requires excel to be installed.
// This uses OpenXML library to achieve this and doesnt require excel to be installed: https://medium.com/swlh/openxml-sdk-brief-guide-c-7099e2391059
string filepath = "Test.xlsx";
// Create a spreadsheet document by supplying the filepath.
// By default, AutoSave = true, Editable = true, and Type = xlsx.
SpreadsheetDocument spreadsheetDocument = SpreadsheetDocument.
Create(filepath, SpreadsheetDocumentType.Workbook);
// Add a WorkbookPart to the document.
WorkbookPart workbookpart = spreadsheetDocument.AddWorkbookPart();
workbookpart.Workbook = new Workbook();
// Add a WorksheetPart to the WorkbookPart.
WorksheetPart worksheetPart = workbookpart.AddNewPart<WorksheetPart>();
worksheetPart.Worksheet = new Worksheet(new SheetData());
// Add Sheets to the Workbook.
Sheets sheets = spreadsheetDocument.WorkbookPart.Workbook.
AppendChild<Sheets>(new Sheets());
// Append a new worksheet and associate it with the workbook.
Sheet sheetOne = new Sheet()
{
Id = spreadsheetDocument.WorkbookPart.
GetIdOfPart(worksheetPart),
SheetId = 1,
Name = "Sheet1"
};
Sheet sheetTwo= new Sheet()
{
Id = spreadsheetDocument.WorkbookPart.
GetIdOfPart(worksheetPart),
SheetId = 2,
Name = "Sheet2"
};
Sheet sheetParameters = new Sheet()
{
Id = spreadsheetDocument.WorkbookPart.
GetIdOfPart(worksheetPart),
SheetId = 3,
Name = "Parameters"
};
Sheet sheetFour= new Sheet()
{
Id = spreadsheetDocument.WorkbookPart.
GetIdOfPart(worksheetPart),
SheetId = 4,
Name = "Sheet4"
};
Sheet sheetFive= new Sheet()
{
Id = spreadsheetDocument.WorkbookPart.
GetIdOfPart(worksheetPart),
SheetId = 5,
Name = "Sheet5"
};
sheets.Append(sheetOne);
sheets.Append(sheetTwo);
sheets.Append(sheetParameters);
sheets.Append(sheetFour);
sheets.Append(sheetFive);
workbookpart.Workbook.Save();
// Close the document.
spreadsheetDocument.Close();
//InsertTextExistingExcel(filepath, "test1", "A", 1, "Parameters");
InsertInSheet(filepath, "test1", "A", 1, "Parameters");
//Any of these lists empty?
}
public static void InsertInSheet(string filePath, string value, string columnName, uint rowIndex, string sheetName)
{
using (SpreadsheetDocument spreadSheet = SpreadsheetDocument.Open(filePath, true))
{
getSheetDetails(spreadSheet);
}
}
//work here
public static void getSheetDetails(SpreadsheetDocument doc)
{
WorksheetPart worksheetPart;
//iterate through sheets
foreach (Sheet sheetDetail in doc.WorkbookPart.Workbook.Sheets)
{
if (sheetDetail.Name == "Parameters")
{
//each sheet has a worksheetPart
worksheetPart = (WorksheetPart)doc.WorkbookPart.GetPartById(sheetDetail.Id);
Cell cell = InsertCellInWorksheet("A", 1, worksheetPart);
cell.CellValue = new CellValue("test");
cell.DataType = new EnumValue<CellValues>(CellValues.String);
worksheetPart.Worksheet.Save();
}
}
}
private static Cell InsertCellInWorksheet(string columnName, uint rowIndex, WorksheetPart worksheetPart)
{
//fix earlier worksheetPart only has a count of 1
Worksheet worksheet = worksheetPart.Worksheet;
//fix this to point to sheet
//SheetData sheetData = worksheet.Descendants<SheetData>();
SheetData sheetData = worksheet.GetFirstChild<SheetData>();
MessageBox.Show(sheetData.InnerXml.ToString());
string cellReference = columnName + rowIndex;
Row row;
if (sheetData.Elements<Row>().Where(r => r.RowIndex == rowIndex).Count() != 0)
{
row = sheetData.Elements<Row>().Where(r => r.RowIndex == rowIndex).First();
}
else
{
row = new Row() { RowIndex = rowIndex };
sheetData.Append(row);
}
Cell refCell = row.Descendants<Cell>().LastOrDefault();
Cell newCell = new Cell() { CellReference = cellReference };
row.InsertAfter(newCell, refCell);
worksheet.Save();
return newCell;
}
Related
There is code which works with Excel like this:
using SpreadsheetDocument spreadsheetDocument = SpreadsheetDocument.Open("filePath", true);
WorkbookPart workbookPart = spreadsheetDocument.WorkbookPart!;
SheetData sheetData = GetSheetData(workbookPart, outFile);
Worksheet worksheet = sheetData.Parent as Worksheet;
List<SharedStringItem> sharedStringTableElements = workbookPart.SharedStringTablePart!.SharedStringTable.Elements<SharedStringItem>().ToList();
List<Row> sheetRows = sheetData.Elements<Row>().ToList();
...
But it breaks when I try to open my generated file. SharedStringTablePart is null.
This is how I create Excel files:
using SpreadsheetDocument spreadSheet = CreateSpreadsheet(filePath);
WorkbookPart workbookPart = spreadSheet.AddWorkbookPart();
workbookPart.Workbook = new Workbook();
WorksheetPart worksheetPart = workbookPart.AddNewPart<WorksheetPart>();
SheetData sheetData = new();
worksheetPart.Worksheet = new Worksheet(sheetData);
Sheets sheets = workbookPart.Workbook.AppendChild(new Sheets());
string relationshipId = workbookPart.GetIdOfPart(worksheetPart);
Sheet sheet = new() {
Id = relationshipId,
SheetId = FirstSheetId,
Name = "SheetName",
};
sheets.Append(sheet);
//fill sheetData here
workbookPart.Workbook.Save();
spreadSheet.Close();
How do I change this code so it doesn't break when accessing a SharedStringTablePart? How can I fill it in correctly?
Thanks in advance.
You create a new SpreadsheetDocument and add the WorkbookPart. Right after being created, that new WorkbookPart does not have any associated parts. Thus, you'll have to create all required parts, including the SharedStringTablePart, like so:
// Let's assume you created the WorkbookPart as follows.
WorkbookPart workbookPart = spreadSheet.AddWorkbookPart();
// Create and initialize the associated SharedStringTablePart as well.
var sharedStringTablePart = workbookPart.AddNewPart<SharedStringTablePart>();
sharedStringTablePart.SharedStringTable = new SharedStringTable();
Once added as shown above, workbookPart.SharedStringTablePart will no longer be null.
I came back to write how I did it.
First of all, we need to create SharedStringTablePart as Thomas said:
private static SharedStringTablePart GetSharedStringTablePart(SpreadsheetDocument spreadSheet)
{
if (spreadSheet.WorkbookPart!.GetPartsOfType<SharedStringTablePart>().Any())
{
return spreadSheet.WorkbookPart.GetPartsOfType<SharedStringTablePart>().First();
}
else
{
return spreadSheet.WorkbookPart.AddNewPart<SharedStringTablePart>();
}
}
Then I created a helper method to insert the data into this table:
private static string InsertSharedStringItem(string text, SharedStringTablePart shareStringPart)
{
shareStringPart.SharedStringTable ??= new SharedStringTable();
int i = 0;
foreach (SharedStringItem item in shareStringPart.SharedStringTable.Elements<SharedStringItem>())
{
if (item.InnerText == text)
{
return i.ToString();
}
i++;
}
shareStringPart.SharedStringTable.AppendChild(new SharedStringItem(new Text(text)));
shareStringPart.SharedStringTable.Save();
return i.ToString();
}
So next time when I inserting cell value I do it like this:
string index = InsertSharedStringItem("some text", sharedTablePart);
Cell cell = new() {
DataType = CellValues.SharedString,
CellValue = new CellValue(index),
...
};
row.AppendChild(cell);
The following method creates a new workbook, and inserts two validator dropdowns:
public static void ForValidator() {
using (SpreadsheetDocument myDoc = SpreadsheetDocument.Create("validator output.xlsx", SpreadsheetDocumentType.Workbook)) {
WorkbookPart workbookpart = myDoc.AddWorkbookPart();
workbookpart.Workbook = new Workbook();
WorksheetPart worksheetPart = workbookpart.AddNewPart<WorksheetPart>();
SheetData sheetData_ = new SheetData();
worksheetPart.Worksheet = new Worksheet(sheetData_);
Sheets sheets_ = myDoc.WorkbookPart.Workbook.AppendChild(new Sheets());
sheets_.AppendChild(new Sheet() {
Id = myDoc.WorkbookPart.GetIdOfPart(myDoc.WorkbookPart.WorksheetParts.First()), SheetId = 1, Name = "Sheet1"
});
DataValidations dataValidations = new DataValidations();
DataValidation dataValidation = new DataValidation() {
Type = DataValidationValues.List, AllowBlank = true, SequenceOfReferences = new ListValue<StringValue>() { InnerText = "F2:F3" }
};
Formula1 formula = new Formula1();
formula.Text = "\"Selection 1,Selection 2,Selection 3\"";
dataValidation.Append(formula);
dataValidations.Append(dataValidation);
worksheetPart.Worksheet.AppendChild(dataValidations);
}
}
Can the same routine be modified to instead just open an existing "validator output.xlsx" file and do the same thing?
For over a week I've tried everything I know to try and have come up with nothing. Thank you for any help.
I am trying to create a simple excel file with multiple sheets using open xml, unfortunately the file does not open after it's being created.
After the file is generated, when I open it with Microsoft Excel it says
We found a problem, do you want to recover as much as we can?
using (SpreadsheetDocument spreedDoc = SpreadsheetDocument.Create(filePath,
DocumentFormat.OpenXml.SpreadsheetDocumentType.Workbook))
{
WorkbookPart wbPart = spreedDoc.WorkbookPart;
wbPart = spreedDoc.AddWorkbookPart();
wbPart.Workbook = new Workbook();
Sheets sheets = wbPart.Workbook.AppendChild(new Sheets());
foreach (var sheetData in excelSheetData)
{
// Add a blank WorksheetPart.
WorksheetPart worksheetPart = wbPart.AddNewPart<WorksheetPart>();
worksheetPart.Worksheet = new Worksheet(new SheetData());
string relationshipId = wbPart.GetIdOfPart(worksheetPart);
// Get a unique ID for the new worksheet.
uint sheetId = 1;
if (sheets.Elements<Sheet>().Count() > 0)
{
sheetId = sheets.Elements<Sheet>().Select(s => s.SheetId.Value).Max() + 1;
}
// Give the new worksheet a name.
string sheetNameToWrite = sheetName;
if (string.IsNullOrWhiteSpace(sheetNameToWrite))
{
sheetNameToWrite = "Sheet"+sheetId;
}
// Append the new worksheet and associate it with the workbook.
Sheet sheet = new Sheet() { Id = relationshipId, SheetId = sheetId, Name = sheetName };
sheets.AppendChild(sheet);
}
//wbPart.Workbook.Sheets.AppendChild(sheet);
wbPart.Workbook.Save();
}
On trying to Repair in excel gives below message
-<repairedRecords summary="Following is a list of repairs:">
<repairedRecord>Repaired Records: Worksheet properties from /xl/workbook.xml part (Workbook)</repairedRecord>
</repairedRecords>
</recoveryLog>
Have you seen this?
http://www.mikesknowledgebase.com/pages/CSharp/ExportToExcel.htm
The necessary steps to create a functional excel file with multiple worksheets in OpenXML (that work for me) are as follows:
using (SpreadsheetDocument spreadsheet = SpreadsheetDocument.Create(path, SpreadsheetDocumentType.Workbook))
{
spreadsheet.AddWorkbookPart();
spreadsheet.WorkbookPart.Workbook = new DocumentFormat.OpenXml.Spreadsheet.Workbook();
spreadsheet.WorkbookPart.Workbook.Append(new BookViews(new WorkbookView()));
WorkbookStylesPart workbookStylesPart = spreadsheet.WorkbookPart.AddNewPart<WorkbookStylesPart>("rIdStyles");
Stylesheet stylesheet = new Stylesheet();
workbookStylesPart.Stylesheet = stylesheet;
workbookStylesPart.Stylesheet.Save();
for (int worksheetNo = 1; worksheetNo < worksheetCountYouWantToCreate; worksheetNo++)
{
string workSheetID = "rId" + worksheetNo;
string worksheetName = "worksheet" + worksheetNo;
WorksheetPart newWorksheetPart = spreadsheet.WorkbookPart.AddNewPart<WorksheetPart>();
newWorksheetPart.Worksheet = new DocumentFormat.OpenXml.Spreadsheet.Worksheet();
newWorksheetPart.Worksheet.AppendChild(new DocumentFormat.OpenXml.Spreadsheet.SheetData());
// write data here
// ...
// ...
newWorksheetPart.Worksheet.Save();
if (worksheetNo == 1)
spreadsheet.WorkbookPart.Workbook.AppendChild(new DocumentFormat.OpenXml.Spreadsheet.Sheets());
spreadsheet.WorkbookPart.Workbook.GetFirstChild<DocumentFormat.OpenXml.Spreadsheet.Sheets>().AppendChild(new DocumentFormat.OpenXml.Spreadsheet.Sheet()
{
Id = spreadsheet.WorkbookPart.GetIdOfPart(newWorksheetPart),
SheetId = (uint)worksheetNo,
Name = worksheetName
});
}
spreadsheet.WorkbookPart.Workbook.Save();
}
I am currently doing my head in working over the OpenXML 2.5 Framework on the MSDN site here, https://msdn.microsoft.com/en-us/library/office/cc861607.aspx
All methods I have tried to add a cell to an existing worksheet corrupt the workbook as the MSDN site only outlines creating the worksheet and not modifying it.
Everytime I add a cell the system wants a whole new worksheet and will not allow the addition of a cell to an existing worksheet. I have been banging my head for hours going over MSDN and Googling this with no luck.
The problem is I need a class that can receiving strings and update the excel file. Has anyone been able to add a cell to an existing worksheet? My issue seems to be due to a string by string solution.
Working input (PowerShell) only works if a new Worksheet is created for the Cell,
[CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')]
$cSharpData = (
[Reflection.Assembly]::LoadWithPartialName("DocumentFormat.OpenXml"),
[Reflection.Assembly]::LoadWithPartialName("WindowsBase"),
[Reflection.Assembly]::LoadWithPartialName("System.Linq")
)
[String]$cSharpClass = Get-Content .\method.cs
$cSharpType = Add-Type -ReferencedAssemblies $cSharpData -TypeDefinition $cSharpClass
$testData = Get-WmiObject Win32_QuickFixEngineering
[DoExcelMethod]::CreateXLSX('.\test.xlsx')
$locNo = 1
[DoExcelMethod]::AddSheetData('.\test.xlsx', $testData, 'TestWS', 'A', $locNo)
The file this is point at has the following,
using System;
using System.Linq;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Spreadsheet;
public class DoExcelMethod {
private static int SharedDataItem(string sData, SharedStringTablePart ssPart) {
if (ssPart.SharedStringTable == null) {
ssPart.SharedStringTable = new SharedStringTable();
}
int cnt = 0;
foreach (SharedStringItem sspItem in ssPart.SharedStringTable.Elements<SharedStringItem>()) {
if (sspItem.InnerText == sData) {
return cnt;
}
cnt++;
}
ssPart.SharedStringTable.AppendChild(new SharedStringItem(new DocumentFormat.OpenXml.Spreadsheet.Text(sData)));
ssPart.SharedStringTable.Save();
return cnt;
}
private static WorksheetPart InsertWorksheet(string wsName, WorkbookPart wbPart) {
WorksheetPart newWsPart = wbPart.AddNewPart<WorksheetPart>();
newWsPart.Worksheet = new Worksheet(new SheetData());
newWsPart.Worksheet.Save();
Sheets sheets = wbPart.Workbook.GetFirstChild<Sheets>();
string relId = wbPart.GetIdOfPart(newWsPart);
uint sheetId = 1;
if (sheets.Elements<Sheet>().Count() > 0) {
sheetId = sheets.Elements<Sheet>().Select(s => s.SheetId.Value).Max() + 1;
}
Sheet sheet = new Sheet() { Id = relId, SheetId = sheetId, Name = wsName };
sheets.Append(sheet);
wbPart.Workbook.Save();
return newWsPart;
}
private static Cell InsertCellInWorksheet(string columnName, uint rowIndex, WorksheetPart worksheetPart) {
Worksheet worksheet = worksheetPart.Worksheet;
SheetData sheetData = worksheet.GetFirstChild<SheetData>();
string cellReference = columnName + rowIndex;
Row row;
if (sheetData.Elements<Row>().Where(r => r.RowIndex == rowIndex).Count() != 0) {
row = sheetData.Elements<Row>().Where(r => r.RowIndex == rowIndex).First();
} else {
row = new Row() { RowIndex = rowIndex };
sheetData.Append(row);
}
if (row.Elements<Cell>().Where(c => c.CellReference.Value == columnName + rowIndex).Count() > 0) {
return row.Elements<Cell>().Where(c => c.CellReference.Value == cellReference).First();
} else {
Cell refCell = null;
foreach (Cell cell in row.Elements<Cell>()) {
if (string.Compare(cell.CellReference.Value, cellReference, true) > 0) {
refCell = cell;
break;
}
}
Cell newCell = new Cell() { CellReference = cellReference };
row.InsertBefore(newCell, refCell);
worksheet.Save();
return newCell;
}
}
public static void CreateXLSX(string xlsxFile) {
SpreadsheetDocument spreadsheetDocument = SpreadsheetDocument.Create(xlsxFile, SpreadsheetDocumentType.Workbook);
WorkbookPart workbookpart = spreadsheetDocument.AddWorkbookPart();
workbookpart.Workbook = new Workbook();
WorksheetPart worksheetPart = workbookpart.AddNewPart<WorksheetPart>();
worksheetPart.Worksheet = new Worksheet(new SheetData());
Sheets sheets = spreadsheetDocument.WorkbookPart.Workbook.AppendChild<Sheets>(new Sheets());
Sheet sheet = new Sheet() { Id = spreadsheetDocument.WorkbookPart.GetIdOfPart(worksheetPart), SheetId = 1, Name = "Default" };
sheets.Append(sheet);
workbookpart.Workbook.Save();
spreadsheetDocument.Close();
}
public static void AddSheetData(string xlsxFile, string psData, string wsName, string psCol, uint psRow) {
using (SpreadsheetDocument sSheet = SpreadsheetDocument.Open(xlsxFile, true)) {
SharedStringTablePart ssPart;
if (sSheet.WorkbookPart.GetPartsOfType<SharedStringTablePart>().Count() > 0) {
ssPart = sSheet.WorkbookPart.GetPartsOfType<SharedStringTablePart>().First();
} else {
ssPart = sSheet.WorkbookPart.AddNewPart<SharedStringTablePart>();
}
int ssIns = SharedDataItem(psData, ssPart);
WorksheetPart wsPart = InsertWorksheet(wsName, sSheet.WorkbookPart);
Cell cell = InsertCellInWorksheet(psCol, psRow, wsPart);
cell.CellValue = new CellValue(ssIns.ToString());
cell.DataType = new EnumValue<CellValues>(CellValues.SharedString);
wsPart.Worksheet.Save();
}
}
}
So despite this working I cannot get a cell into an existing worksheet, can anyone help as I am going insane :(
Thanks all
The issue you have is in the call to InsertWorksheet in AddSheetData. You are calling the InsertWorksheet method irrespective of whether or not the worksheet already exists. Instead of doing that, you can first search for the worksheet then if it exists you can use it and if it doesn't you can create a new one.
Firstly, you can search for a WorksheetPart by its name using a method such as this one (taken from my answer here):
private static WorksheetPart GetWorksheetPartBySheetName(WorkbookPart workbookPart, string sheetName)
{
WorksheetPart worksheetPart = null;
//find the sheet (note this is case-sensitive)
IEnumerable<Sheet> sheets = workbookPart.Workbook.GetFirstChild<Sheets>().Elements<Sheet>().Where(s => s.Name == sheetName);
if (sheets.Count() > 0)
{
string relationshipId = sheets.First().Id.Value;
worksheetPart = (WorksheetPart)workbookPart.GetPartById(relationshipId);
}
return worksheetPart;
}
If that method finds the WorksheetPart then it will return it, if not it will return null.
Once you have that you just need a small tweak to AddSheetData to call GetWorksheetPartBySheetName then only call InsertWorksheet if that method returns null. To do that you can replace this line
WorksheetPart wsPart = InsertWorksheet(wsName, sSheet.WorkbookPart);
with this
WorksheetPart wsPart = GetWorksheetPartBySheetName(sSheet.WorkbookPart, wsName);
if (wsPart == null)
wsPart = InsertWorksheet(wsName, sSheet.WorkbookPart);
I've been using OPEN XML to create a spread sheet which can then be downloaded. I want to set the column widths and also change the header to bold. I've done this using Office.Interop.Excel but I'm struggling with this format. My code is below, you can see I've set up a font but have been unable to assign it to my spread sheet, I have got messages saying it's not possible to assign to a tree.
SpreadsheetDocument spreadsheetDocument = SpreadsheetDocument.Create(fileName, SpreadsheetDocumentType.Workbook); //Open(fileName, true);
try
{
WorkbookPart workbookpart = spreadsheetDocument.AddWorkbookPart();
workbookpart.Workbook = new DocumentFormat.OpenXml.Spreadsheet.Workbook();
WorksheetPart worksheetPart = workbookpart.AddNewPart<WorksheetPart>();
worksheetPart.Worksheet = new DocumentFormat.OpenXml.Spreadsheet.Worksheet(new SheetData());
DocumentFormat.OpenXml.Spreadsheet.Sheets sheets = spreadsheetDocument.WorkbookPart.Workbook.AppendChild<DocumentFormat.OpenXml.Spreadsheet.Sheets>(new DocumentFormat.OpenXml.Spreadsheet.Sheets());
Sheet sheet = new Sheet() { Id = spreadsheetDocument.WorkbookPart.GetIdOfPart(worksheetPart), SheetId = 1, Name = "Models" };
sheets.Append(sheet);
DocumentFormat.OpenXml.Spreadsheet.Font boldFont = new DocumentFormat.OpenXml.Spreadsheet.Font();
Bold bFontBold = new Bold();
boldFont.Append(bFontBold);
DocumentFormat.OpenXml.Spreadsheet.Worksheet worksheet = new DocumentFormat.OpenXml.Spreadsheet.Worksheet();
SheetData sheetData = new SheetData();
........
Row row = new Row();
Cell cell = new Cell()
{
CellReference = "A" + (intI + 1),
DataType = CellValues.String,
CellValue = new CellValue(ModelBookrowDetail[0])
};
row.Append(cell);
Cell cell2 = new Cell()
{
CellReference = "B" + (intI + 1),
DataType = CellValues.String,
CellValue = new CellValue(ModelBookrowDetail[1])
};
row.Append(cell2);
.........
sheetData.Append(row);
}
worksheet.Append(sheetData);
worksheetPart.Worksheet = worksheet;
spreadsheetDocument.Close();
}
catch
{
spreadsheetDocument.Close();
}
I don't know how set you are on using OpenXml. I might suggest looking into ClosedXml. Up until now it has saved me a lot of manhours. It only has one downside to it, and that is that it doesn't manage Word documents like OpenXml does.
ClosedXml is a more object oriented Open Source Library, and is far more well documented in contrast to what i have experienced with OpenXml.
You can find ClosedXml here:
https://github.com/ClosedXML/ClosedXML
Best of luck
Olliver