C# Find empty cells and write them inside with ClosedXml - c#

I have this problem, I have installed ClosedXml:
I have an Excel file already created and populated, now I should find the blank line below the already populated one and write some data
Example:
[A, 1] = name;
[B, 1] = surname;
the next line will be empty and I will pass some variables to populate the cells going to the right.
OpenFileDialog FileExcel = new OpenFileDialog();
if (FileExcel.ShowDialog() == DialogResult.OK)
{
try
{
var sr = new StreamReader(FileExcel.FileName);
}
catch (SecurityException ex)
{
MessageBox.Show($"Security error.\n\nError message: {ex.Message}\n\n" +
$"Details:\n\n{ex.StackTrace}");
}
}
using (var excelWorkbook = new XLWorkbook(FileExcel.FileName))
{
var nonEmptyDataRows = excelWorkbook.Worksheet(Convert.ToInt32(comboBox1.SelectedItem)).RowsUsed();
foreach (var dataRow in nonEmptyDataRows)
{
//for row number check
if (dataRow.RowNumber() >= 1 && dataRow.RowNumber() <= 100)
{
}
}
}

Use row.Cells(false) instead of row.Cells(). It does not skip over unused cells. Then you can simply check of cell.Value() is empty

you can do something like that
int lastrow = worksheet.LastRowUsed().RowNumber();
var rows = worksheet.Rows(1, lastrow);
foreach (IXLRow row in rows)
{
foreach (IXLCell cell in row.Cells())
{
if (cell.IsEmpty())
{
//do something
}
}
}

Related

Find linked formula values from worksheets and replace with actual cell value

In a OOXML spreadsheet .xlsx you can through a linking formula fecth values from another spreadsheet and have them in your worksheet as values, that will always be updated when those values in another spreadsheet are updated.
I am using Open Xml SDK and I basically want to do what this does: https://www.e-iceblue.com/Tutorials/Spire.XLS/Spire.XLS-Program-Guide/Formula/Remove-Formulas-from-Cells-but-Keep-Values-in-Excel-in-C.html
How do I:
Find a value that has formula linking value to a cell in another spreadsheet
Replace the formula value with the actual cell value
Do this foreach cell in each worksheet in a spreadsheet
I have tried this so far: https://learn.microsoft.com/en-us/office/open-xml/how-to-retrieve-the-values-of-cells-in-a-spreadsheet
But I am recieving a NullRefereceneException each time the cell does not contain a formula or just any value. I have tried try-catch and several other ways to escape this exception, but it is not working.
But back to the challenge as outlined above; can anyone help me out?
Basic stuff such as using SOME DIRECTIVE, foreach loop, Open(), Save() I know how to do.
This worked for me:
public void Remove_CellReferences(string filepath)
{
using (SpreadsheetDocument spreadsheet = SpreadsheetDocument.Open(filepath, true))
{
// Delete all cell references in worksheet
List<WorksheetPart> worksheetparts = spreadsheet.WorkbookPart.WorksheetParts.ToList();
foreach (WorksheetPart part in worksheetparts)
{
Worksheet worksheet = part.Worksheet;
var rows = worksheet.GetFirstChild<SheetData>().Elements<Row>(); // Find all rows
foreach (var row in rows)
{
var cells = row.Elements<Cell>();
foreach (Cell cell in cells)
{
if (cell.CellFormula != null)
{
string formula = cell.CellFormula.InnerText;
if (formula.Length > 0)
{
string hit = formula.Substring(0, 1); // Transfer first 1 characters to string
if (hit == "[")
{
CellValue cellvalue = cell.CellValue; // Save current cell value
cell.CellFormula = null; // Remove RTD formula
// If cellvalue does not have a real value
if (cellvalue.Text == "#N/A")
{
cell.DataType = CellValues.String;
cell.CellValue = new CellValue("Invalid data removed");
}
else
{
cell.CellValue = cellvalue; // Insert saved cell value
}
}
}
}
}
}
}
// Delete all external link references
List<ExternalWorkbookPart> extwbParts = spreadsheet.WorkbookPart.ExternalWorkbookParts.ToList();
if (extwbParts.Count > 0)
{
foreach (ExternalWorkbookPart extpart in extwbParts)
{
var elements = extpart.ExternalLink.ChildElements.ToList();
foreach (var element in elements)
{
if (element.LocalName == "externalBook")
{
spreadsheet.WorkbookPart.DeletePart(extpart);
}
}
}
}
// Delete calculation chain
CalculationChainPart calc = spreadsheet.WorkbookPart.CalculationChainPart;
spreadsheet.WorkbookPart.DeletePart(calc);
}
}

Getting Exponential value from Excel column

I created a function that reads an Excel sheet. So there is a field in the sheet that contains a value of 0.01, but when I read this value it change into some exponential form like this 1.2999999999999999E-2, so it throws exception when I pass the value to the datatable.
How can this value be read accuratly OR avoided in code?
Here is my code to read the Excel sheet -
using (var sDoc = SpreadsheetDocument.Open(FileName, false))
{
bool emptyTable = false;
var sheets = sDoc.WorkbookPart.Workbook.GetFirstChild<Sheets>().Elements<Sheet>();
var relationshipId = sheets.First().Id.Value;
var worksheetPart = (WorksheetPart)sDoc.WorkbookPart.GetPartById(relationshipId);
var workSheet = worksheetPart.Worksheet;
var sheetData = workSheet.GetFirstChild<SheetData>();
var rows = sheetData.Descendants<Row>().Skip(IgnoreRows).ToList();
if (rows.Count == 0)
{
Results = dt;
emptyTable = true;
}
if (!emptyTable)
{
foreach (var cell in rows.ElementAt(0).Cast<Cell>())
{
dt.Columns.Add(GetCellValue(sDoc, cell));
}
foreach (var row in rows.Skip(1))
{
bool emptyRow = true;
var tempRow = dt.NewRow();
for (var i = 0; i < row.Descendants<Cell>().Count(); i++)
{
string rowValue = GetCellValue(sDoc, row.Descendants<Cell>().ElementAt(i));
tempRow[i] = rowValue;
emptyRow = emptyRow && String.IsNullOrWhiteSpace(rowValue);
}
if (!emptyRow)
{
dt.Rows.Add(tempRow);
}
else
{
break;
}
}
}
And here is the exception value:
You can try something like this (code block inside your for loop)
string rowValue = GetCellValue(sDoc, row.Descendants<Cell>().ElementAt(i));
if (i == 5) // index of column where you're expecting that value
{
decimal tempDecimal;
decimal.TryParse(rowValue, NumberStyles.AllowExponent | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out tempDecimal);
rowValue = tempDecimal.ToString("0.##");
}
tempRow[i] = rowValue;
emptyRow = emptyRow && String.IsNullOrWhiteSpace(rowValue)
Basically, code is trying to parse value from specific column (5 in my example) to decimal and then converts it back to string with two decimal points.

Open XML not saving when adding data C#

I did a complete copy from the following link: https://msdn.microsoft.com/en-us/library/dd452407(v=office.12).aspx
The copy from template works fine, the FixChartData() method works fine. However, the output File does not contain any data. I do see that the contentRow contains the data via the debugger, but the excel sheet does not have the data in it when I open the file.
Very frustrating. Any help would be appreciated.
public void Create()
{
string appPath = System.IO.Path.GetDirectoryName(System.IO.Path.GetDirectoryName(System.IO.Directory.GetCurrentDirectory()));
string templateFile = appPath + #"\Templates\ChartExample.xlsx";
string saveFile = appPath + #"\Documents\Generated.xlsx";
File.Copy(templateFile, saveFile, true);
//open copied template.
using(SpreadsheetDocument myWorkbook = SpreadsheetDocument.Open(saveFile, true))
{
//this is the workbook contains all the worksheets
WorkbookPart workbookPart = myWorkbook.WorkbookPart;
//we know that the first worksheet contains the data for the graph
WorksheetPart worksheetPart = workbookPart.WorksheetParts.First(); //getting the first worksheet
//the shhet data contains the information we are looking to alter
SheetData sheetData = worksheetPart.Worksheet.GetFirstChild<SheetData>();
int index = 2;//Row the data for the graph starts on
//var qry = from t in db.SEL_SE_DEATHS()
FudgeData fudge = new FudgeData();
var qry = fudge.Fudged();
foreach(var item in qry)
{
int Year = item.EventYear;
int PSQ = item.PSQReviewable;
int death = item.Deaths;
Row contentRow = CreateContentRow(index, Year, PSQ, death);
index++;
//contentRow.RowIndex = (UInt32)index;
sheetData.AppendChild(contentRow);
}
//(<x:c r="A2" xmlns:x="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><x:v>2014</x:v></x:c><x:c r="B2" xmlns:x="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><x:v>21</x:v></x:c><x:c r="C2" xmlns:x="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><x:v>4</x:v></x:c>)
FixChartData(workbookPart, index);
worksheetPart.Worksheet.Save();
myWorkbook.Close();
myWorkbook.Dispose();
}
}
string[] headerColumns = new string[] { "A", "B", "C" }; //the columns being accessed
public Row CreateContentRow(int index, int year, int pSQ, int death)
{
Row r = new Row();
r.RowIndex = (UInt32)index;
//skipping the text add function
//we are createing a cell for each column (headerColumns),
//for each cell we are adding a value.
//we then append the value to the cell and append the cell to the row - wich is returned.
for(int i =0; i <headerColumns.Length; i++)
{
Cell c = new Cell();
c.CellReference = headerColumns[i] + index;
CellValue v = new CellValue();
if(i == 0)
{
v.Text = year.ToString();
}else if(i == 1)
{
v.Text = pSQ.ToString();
}else if(i == 2)
{
v.Text = death.ToString();
}
c.AppendChild(v);
r.AppendChild(c);
}
return r;
}
//Method for when the datatype is text based
public Cell CreateTextCell(string header, string text, int index)
{
//Create a new inline string cell.
Cell c = new Cell();
c.DataType = CellValues.InlineString;
c.CellReference = header + index;
//Add text to the text cell.
InlineString inlineString = new InlineString();
Text t = new Text();
t.Text = text;
inlineString.AppendChild(t);
c.AppendChild(inlineString);
return c;
}
//fix the chart Data Regions
public void FixChartData(WorkbookPart workbookPart, int totalCount)
{
var wsparts = workbookPart.WorksheetParts.ToArray();
foreach(WorksheetPart wsp in wsparts)
{
if(wsp.DrawingsPart != null)
{
ChartPart chartPart = wsp.DrawingsPart.ChartParts.First();
////change the ranges to accomodate the newly inserted data.
foreach (DocumentFormat.OpenXml.Drawing.Charts.Formula formula in chartPart.ChartSpace.Descendants<DocumentFormat.OpenXml.Drawing.Charts.Formula>())
{
if (formula.Text.Contains("$2"))
{
string s = formula.Text.Split('$')[1];
formula.Text += ":$" + s + "$" + totalCount;
}
}
chartPart.ChartSpace.Save();
}
}
//ChartPart chartPart = workbookPart.ChartsheetParts.First().DrawingsPart.ChartParts.First();
////change the ranges to accomodate the newly inserted data.
//foreach(DocumentFormat.OpenXml.Drawing.Charts.Formula formula in chartPart.ChartSpace.Descendants<DocumentFormat.OpenXml.Drawing.Charts.Formula>())
//{
// if (formula.Text.Contains("$2"))
// {
// string s = formula.Text.Split('$')[1];
// formula.Text += ":$" + s + "$" + totalCount;
// }
//}
//chartPart.ChartSpace.Save();
}
David,
I got your code to work fine. Here is a link to my Console Application.. I uploaded it to Github with some minor changes. I made 2 changes:
1) I was not able to download the samples from the link you provided. So I created a blank empty spreadsheet with Excel2016 and saved it in that directory.
2) The Fudge data was missing, so I generated some sample data via self mocked object.
The spreadsheet copies fine from the template and your code populates it with the fudge data. Here is what the final result looks like:
After downloading, you will need to make a Template and Document subdirectory. Then place my ChartExample.xslx file in the Template directory and run.

Ordering a ConcurrentDictionary. Why is this not working?

We have a C# app that populates tables on worksheets within an Excel document.
The tables must be populated in the order the rows are returned from the database.
The object DataFileColData is defined as a List and contains the result set rows. For testing purposes, I'm only using [0] of the List.
Code segment #1 below doesn't work. Row order is not preserved in that the end result has the data displayed out of order although the numbers themselves are listed in order:
if (DataFileColData[0].Count() > 0)
{
ConcurrentDictionary<int, DataRow> theRows = new ConcurrentDictionary<int, DataRow>(9, DataFileColData[0].Count());
Parallel.For(0, DataFileColData[0].Count(), i =>
{
// go through each column
int c = 0;
try
{
foreach (var Col in DataFileColData)
{
var cell = Col[i];
if (cell != null)
{
if (cell.GetType().Name == "JArray") //If Jarray then table compression was used not column compression
{
if (theRows.TryAdd(i, Dt.NewRow()))
theRows[i].ItemArray = JsonConvert.DeserializeObject<object[]>(Col[i].ToString());
}
else
{
if (theRows.TryAdd(i, Dt.NewRow()))
theRows[i][c] = cell;
}
}
c++;
}
} //try
catch (Exception e)
{
throw new Exception("Exception thrown in \"PublicMethods.cs | RenderExcelFile\" while in foreach loop over DataFileColData: " + e.ToString());
}
} //for
); //parallel
//Add the rows to the datatable in their original order
//(might have gotten skewed from the parallel.for loop)
for (int x = 0; x < theRows.Count; x++)
Dt.Rows.Add(theRows[x]);
//Set the name so it appears nicely in the Excel Name Box dropdown instead of "table1", "table2", etc etc.
Dt.TableName = ExcelTableSpec.TableTitle + " " + r.TableID;
}
code segment #2 below does work with the row order and data associated with each row preserved :
if (DataFileColData[0].Count() > 0)
{
DataRow[] theRows = new DataRow[DataFileColData[0].Count()];
Parallel.For(0, DataFileColData[0].Count(), i =>
{
DataRow Rw = Dt.NewRow();
// go through each column
int c = 0;
try
{
foreach (var Col in DataFileColData)
{
var cell = Col[i];
if (cell != null)
{
if (cell.GetType().Name == "JArray") //If Jarray then table compression was used not column compression
{
lock (theRows)
{
theRows[i] = Dt.NewRow();
theRows[i].ItemArray = JsonConvert.DeserializeObject<object[]>(Col[i].ToString());
}
}
else
{
lock (theRows)
{
theRows[i] = Dt.NewRow();
theRows[i][c] = cell;
}
}
}
c++;
}
} //try
catch (Exception e)
{
throw new Exception("Exception thrown in \"PublicMethods.cs | RenderExcelFile\" while in foreach loop over DataFileColData: " + e.ToString());
}
} //for
); //parallel
//Add the rows to the datatable in their original order
//(might have gotten skewed from the parallel.for loop)
Dt = theRows.CopyToDataTable();
//Set the name so it appears nicely in the Excel Name Box dropdown instead of "table1", "table2", etc etc.
Dt.TableName = ExcelTableSpec.TableTitle + " " + r.TableID;
}
I don't understand why. I didn't think the locking mechanism would be needed because each thread gets its own instance of "i" and a ConcurrentDictionary is supposed to be thread safe.
Would someone be able to explain to me please why the code isn't working the way I think it should?
Thank you!
UPDATED CODE as per #Enigmativity's comments below.
The MSDN documentation isn't quite clear (to me anyway), but does appear to update the DataTable even though the MSDN documentation doesn't indicate it does when executing the NewRow() method.
New working code below:
if (DataFileColData[0].Count() > 0)
{
DataRow[] theRows = new DataRow[DataFileColData[0].Count()];
Parallel.For(0, DataFileColData[0].Count(), i =>
//for (int i = 0; i < DataFileColData[0].Count(); i++)
{
lock (Dt)
{
theRows[i] = Dt.NewRow();
}
// go through each column
int c = 0;
try
{
foreach (var Col in DataFileColData)
{
var cell = Col[i];
if (cell != null)
{
if (cell.GetType().Name == "JArray") //If Jarray then table compression was used not column compression
{
theRows[i].ItemArray = JsonConvert.DeserializeObject<object[]>(Col[i].ToString());
}
else
{
theRows[i][c] = cell;
}
}
c += 1;
} //foreach
} //try
catch (Exception e)
{
throw new Exception("Exception thrown in \"PublicMethods.cs | RenderExcelFile\" while in foreach loop over DataFileColData: " + e.ToString());
}
} //for
); //parallel
//Add the rows to the datatable in their original order
//(might have gotten skewed from the parallel.for loop)
Dt = theRows.CopyToDataTable();
//Set the name so it appears nicely in the Excel Name Box dropdown instead of "table1", "table2", etc etc.
Dt.TableName = ExcelTableSpec.TableTitle + " " + r.TableID;
//cleanup
if (theRows != null)
Array.Clear(theRows, 0, theRows.Length);
theRows = null;
} //if (DataFileColData[0].Count() > 0)
Please see the documentation for (MSDN Data Tables).
The key point is:
Thread Safety
This type is safe for multithreaded read operations. You must
synchronize any write operations.
So it's not i the the ConcurrentDictionary causing your issues.
I've decompiled the NewRow method and there is a call to NewRow(int record). This code clearly shows write operations.
internal DataRow NewRow(int record)
{
if (-1 == record)
record = this.NewRecord(-1);
this.rowBuilder._record = record;
DataRow row = this.NewRowFromBuilder(this.rowBuilder);
this.recordManager[record] = row;
if (this.dataSet != null)
this.DataSet.OnDataRowCreated(row);
return row;
}

EPPlus, Find and set the value for a Named Range

I've been pulling my hair out trying to set the value of a named range (in this case, a single named cell) using the ExcelPackage (3.0.1) library, it should be a simple as this:
ExcelNamedRange er = xlPackage.Workbook.Names["Customer"];
er.Value = "Foo Bar";
I'm obviously doing it wrong - has anyone got an example I can follow
Thanks
I looked for ExcelPackage documentation to see what type Names[] collection returns and found that documentatios will come soon, or at least that is what they said back in 2007.
I suggest you use EPPlus wich is a excel library (xlsx only) that have worked great to me.
official link
Now, to set a value for each cell in a named range:
ExcelWorksheet sheet = _openXmlPackage.Workbook.Worksheets["SheetName"];
using (ExcelNamedRange namedRange = sheet.Names["RangeName"])
{
for (int rowIndex = Start.Row; rowIndex <= namedRange.End.Row; rowIndex++)
{
for (int columnIndex = namedRange.Start.Column; columnIndex <= namedRange.End.Column; columnIndex++)
{
sheet.Cells[rowIndex, columnIndex].Value = "no more hair pulling";
}
}
}
I had to put in a work around using a cell value instead.
using (ExcelPackage xlPackage = new ExcelPackage(newFile))
{
foreach (ExcelWorksheet worksheet in xlPackage.Workbook.Worksheets)
{
var dimension = worksheet.Dimension;
if (dimension == null) { continue; }
var cells = from row in Enumerable.Range(dimension.Start.Row, dimension.End.Row)
from column in Enumerable.Range(dimension.Start.Column, dimension.End.Column)
//where worksheet.Cells[row, column].Value.ToString() != String.Empty
select worksheet.Cells[row, column];
try
{
foreach (var excelCell in cells)
{
try
{
if (excelCell.Value.ToString().Equals("[Customer]")) { excelCell.Value = "Customer Name"; }
}
catch (Exception) { }
}
}
catch (Exception a) { Console.WriteLine(a.Message); }
}

Categories