SharpCell

Cells, ranges and dates

Going through the cells of a sheet, reading and writing blocks of values, dates, number formats and the text Excel shows, and the defined names of a workbook.

Going through cells

foreach (Cell cell in sheet.Cells)   // non-empty cells, row by row
{
    if (cell.Formula is not null)
        lines.Add($"{cell.Address}: {cell.Formula} = {cell.Value}");
}

CellRange? used = sheet.UsedRange;   // A1:F6 for the sample budget; null for an empty sheet

Worksheet.Cells visits only cells that hold something: a constant, a formula, or a value a dynamic array spilled into them. Empty cells cost nothing, so a sheet with a cell in XFD1048576 is as quick to walk as any other. CellRange.Cells does the same for part of a sheet.

While enumerating you may change the value or formula of the cells you get. Adding or removing a cell, including setting a value to CellValue.Empty or a recalculation that changes a spill, makes the next step throw InvalidOperationException, as changing a .NET collection during a foreach does.

Ranges

Worksheet.Range takes an A1 address on that sheet: A1:C10, a single cell B2, whole columns A:B or whole rows 2:3. A CellRange is a handle, like a Cell: making one allocates nothing. Its ToString() includes the sheet, as in 'My Sheet'!A1:C10.

CellRange amounts = sheet.Range("B2:B5");
CellValue[,] before = amounts.GetValues();   // [row, column] from B2: 1200, 450, 120, 40

amounts.SetValues(new CellValue[,] { { 1300 }, { 500 }, { 150 }, { 50 } });
workbook.Recalculate();
// sheet["B6"].Value is now 2000

GetValues returns every cell of the range, empty ones as CellValue.Empty, indexed [row, column] from the top-left cell. A whole column has 1,048,576 cells and the array has as many elements; for sparse data use UsedRange or Cells.

SetValues writes the way assigning Cell.Value does: formulas are replaced and CellValue.Empty clears a cell. It checks everything first (the array's size, values a cell cannot hold, cells inside an array formula other than its top-left cell, even when the range covers the whole array) and writes nothing when a check fails. As with any change, formulas see the new values after Workbook.Recalculate.

Dates

Excel stores a date as a number: days counted from the workbook's date system, with the time of day as the fraction. A CellValue does not know which date system its workbook uses, so the conversions take it:

sheet["A1"].Value = CellValue.DateTime(new DateTime(2026, 3, 15), workbook.DateSystem);
sheet["A2"].Formula = "=EOMONTH(A1, 0)";
workbook.Recalculate();

DateTime monthEnd = sheet["A2"].Value.AsDateTime(workbook.DateSystem);   // 2026-03-31

AsDateTime throws InvalidOperationException for a value that is not a number, like the other As methods, and ArgumentOutOfRangeException for a number that is no date: a negative one, one past 9999-12-31, or 60 in the 1900 date system, which Excel shows as the non-existent 29 February 1900. The time is rounded to the millisecond. CellValue.DateTime refuses dates before the date system starts.

Number formats and display text

sheet["A1"].Value = 1234.5;
sheet["A1"].NumberFormat = "#,##0.00";   // as read from the file, or set here
string shown = sheet["A1"].Text;          // "1,234.50"

Cell.NumberFormat is the cell's format code, such as 0.00, #,##0 or yyyy-mm-dd, and General when it has none. XlsxReader reads it from the file's cell styles, Excel's built-in formats included. Cell.Text is the value as Excel shows it through that format, with the workbook's culture for separators and month names: dates come out as dates, percentages with %, and General the way Excel's default column shows a number.

Value Text
Number formatted by the cell's format; a date format on a number that is no date gives #######
Text the format's text section ("Name: "@), or the text itself
Logical TRUE or FALSE
Error #DIV/0!, #N/A and so on
Empty an empty string

Column width plays no part, so a long number is never cut to ####. A format is no content: it does not block a spill and is not listed by Cells. XlsxWriter saves formats with the cells.

Typing into cells

sheet["A1"].Input = "12.5%";          // 0.125, and the format 0.00%
sheet["A2"].Input = "10/11/2026";     // a date in the workbook's culture, and its short date format
sheet["A3"].Input = "=A1*2";          // a formula
sheet["A4"].Input = "'0123";          // text, kept as typed
string edit = sheet["A1"].Input;       // "12.5%", as Excel shows it to edit

Cell.Input is what a person types into a cell in Excel. Setting it does what typing and pressing Enter does: = starts a formula, ' keeps the rest as text, TRUE, FALSE and error literals are those values, and numbers, percentages, currency, grouped digits, dates and times are read in Workbook.Culture by the same rules as text meeting arithmetic and DATEVALUE; anything else stays text, and an empty string clears the cell. A cell in the General format takes a format from what was typed: 12% gives 0%, 1,250.50 gives #,##0.00, $5 a currency format, a date the culture's short date, a time h:mm with seconds and AM/PM when they were typed. A cell with a format of its own keeps it.

Getting Input gives the text Excel shows to edit the cell: the formula, a number in full, a percentage as one, a date or time as the culture writes it, and text with an apostrophe in front when it would read as something else. Typed back, it gives the same content. A format taken from typing is saved with the cell by XlsxWriter. The formats Excel gives typed currency and dates are not yet compared with Excel's.

Styles

CellStyle header = sheet["A1"].Style with   // the workbook's default until the cell has its own
{
    Font = sheet["A1"].Style.Font with { Bold = true },
    Fill = CellColor.FromRgb(0xD9E1F2),
    BottomBorder = new CellBorder(CellBorderStyle.Thin),
    HorizontalAlignment = CellHorizontalAlignment.Center,
};
sheet["A1"].Style = header;
bool bold = sheet["A1"].Style.Font.Bold;      // true
string font = sheet["B1"].Style.Font.Name;    // "Calibri", the default

Cell.Style is how the cell looks in Excel: its CellFont (name, size in points, bold, italic, underline, strikethrough, colour), a fill colour, the four edges of its border and its alignment, wrapping and indent. A cell without a style of its own has Workbook.DefaultStyle, which XlsxReader takes from the file's Normal style; setting Style to null gives it back. Styles are records: they compare by value, and with makes a changed copy.

XlsxReader turns theme and indexed colours into RGB with the file's theme and palette. Only solid fills are read; patterns and gradients read as no fill. Styles of whole rows and columns (for cells that hold nothing), conditional formats, rotated text, shrink to fit and diagonal borders are not read. A style plays no part in calculation and is no content.

Column widths, row heights and merged cells

sheet.SetColumnWidth(2, 20);      // column B, in the units .xlsx files use
sheet.SetRowHeight(1, 30);        // row 1, in points
sheet.SetColumnHidden(3, true);   // column C
sheet.Merge("A1:D1");
double? width = sheet.ColumnWidth(1);           // null: column A has the default width
string merged = sheet.MergedAreas[0].Address;   // "A1:D1"

Column widths are in the units .xlsx files use: characters of the widest digit of the default font, padding included, so Excel's standard 8.43 characters of Calibri 11 are 9.140625 here. Row heights are in points. Null means the sheet's default, Worksheet.DefaultColumnWidth and Worksheet.DefaultRowHeight, which are null too when the file gives none. Converting either to pixels depends on the font and the screen and is left to the application.

Hidden columns change no result, as in Excel; hidden rows do (see Tables and hidden rows). Merge keeps the values of the cells it covers, where Excel's Merge command would clear all but the top-left one.

Frozen panes and gridlines

sheet.FrozenRows = 1;          // the header row stays in place while the rest scrolls
sheet.FrozenColumns = 1;       // and so does column A
sheet.ShowGridlines = false;   // as View > Gridlines turned off in Excel

FrozenRows and FrozenColumns are the rows at the top and the columns on the left that Excel's Freeze Panes keeps in place; ShowGridlines is whether Excel draws gridlines on the sheet. XlsxReader takes them from the sheet's first view. A plain split that is not frozen reads as no frozen panes. None of them changes a result.

XlsxWriter saves styles, sizes, merged cells, frozen panes and gridlines. A new style starts from the cell's style in the file, so what SharpCell does not read (pattern fills, rotated text) stays.

Printing

sheet.PageOrientation = PageOrientation.Landscape;
sheet.PageMargins = PageMargins.Narrow with { Top = 1 };
sheet.PrintArea = "A1:E7";          // only these cells print
sheet.PrintTitleRows = "1:1";       // the header row repeats on every page
sheet.AddRowPageBreak(5);           // a new page starts at row 5

These are the settings of Excel's Page Layout tab. PageOrientation is portrait unless set otherwise, and PageMargins are Excel's Normal margins; PageMargins.Wide and PageMargins.Narrow are its other presets, and with changes single margins, in inches. PrintArea is one range or several separated by commas, each printed on pages of its own; null prints everything. PrintTitleRows and PrintTitleColumns repeat rows at the top and columns at the left of every page. AddRowPageBreak and AddColumnPageBreak start a new page at a row or column, as Insert Page Break does; RowPageBreaks and ColumnPageBreaks list them.

The print area moves with its cells when rows, columns or cells are inserted or deleted, as the defined name Excel keeps it in does; titles and page breaks move with whole rows and columns. XlsxReader reads all of them and XlsxWriter saves them; the file's other page setup (paper size, scaling, headers and footers) stays as it was.

Inserting and deleting rows and columns

sheet.InsertRows(3);             // a new row 3; rows 3 and below move down
sheet["A3"].Value = "Travel";
sheet["B3"].Value = 300;
workbook.Recalculate();          // B7 is =SUM(B2:B6) now and includes the new row

sheet.DeleteColumns(5);          // column E goes; formulas that read it get #REF!

InsertRows, DeleteRows, InsertColumns and DeleteColumns work like Excel's commands. Cells, styles, number formats, row heights, column widths, hidden rows and columns, merged areas and tables move, and every reference to the moved cells follows them: in formulas of every sheet and in defined names. A range the new rows are inserted into grows (SUM(B2:B5) becomes SUM(B2:B6)), and so do merged areas and tables; inserting at a range's first row moves it instead. A range that loses some of its rows shrinks, a reference to a deleted cell becomes #REF!, and a table whose rows are all deleted is removed. Whole-column references such as A:A do not change when rows move. References through several sheets (Sheet1:Sheet3!A1) and text given to INDIRECT do not change either, as in Excel.

Every formula is calculated again at the next Workbook.Recalculate. An edit that would push content past the last row or column, split an array formula (Ctrl+Shift+Enter), or remove a table's header or totals row without the whole table throws InvalidOperationException and changes nothing. Inserting or deleting columns inside a table throws NotSupportedException, and so does any edit of a sheet that has, or is named by, a formula SharpCell cannot read (such as a link to another workbook): its references could not follow. Formulas that name a table whose rows were all deleted get #REF!. Each edit goes through every formula of the workbook, so insert or delete many rows with one call (InsertRows(5, 100)) rather than one at a time. When the workbook is saved, conditional formats, data validation, pictures, comments, charts and pivot table sources in the file move with the cells too (see Saving .xlsx files).

Inserting and deleting cells

sheet.Range("A1").Insert(InsertShift.Down);   // A1 and A2 move to A2 and A3; column B stays
workbook.Recalculate();                        // C1 is =SUM(A2:A3) now

sheet.Range("A1:A2").Delete(DeleteShift.Up);  // A3 moves up to A1; a formula reading A2 gets #REF!

CellRange.Insert and CellRange.Delete work like Excel's Insert and Delete with "Shift cells down", "Shift cells right", "Shift cells up" and "Shift cells left": only the cells in the range's columns (or rows) move, with their styles, number formats, links, data validation, merged areas and tables, and references to them follow as they do for rows. A reference to a range that only some of the moved cells are in stays as it was (SUM(A1:C5) does not change when cells of column A move), and a data validation rule over such a range is split. Row heights and column widths stay. A range of whole rows or whole columns inserts or deletes them, as InsertRows does.

Cells that would move apart from the rest of a merged area, a table or an array formula throw InvalidOperationException and change nothing, as Excel refuses them. When the workbook is saved, the file's conditional formats, comments and other parts move with the cells; pictures and charts stay where they are.

Copying cells

sheet["B1"].Formula = "=A1*$A$1";
sheet.Range("B1").CopyTo(sheet.Range("B2:B3"));   // B2 is =A2*$A$1, B3 is =A3*$A$1
sheet.Range("A1:B3").CopyTo(sheet.Range("D1"));   // values, formulas, formats, styles, links, validation
workbook.Recalculate();

CellRange.CopyTo copies cells as copying and pasting them in Excel does: values, formulas, number formats, styles, hyperlinks, notes and data validation. Relative references in a formula keep their distance from its cell and absolute ones ($A$1) stay; a reference that would point past the sheet's edge becomes #REF!. Data validation formulas keep their distance from each cell in the same way. Empty cells of the range clear the cells they land on, their validation and hyperlinks too. A destination that is a whole number of times as tall and as wide as the range is filled by repeating it, so one cell copied to B2:B3 fills both; any other destination gets one copy at its top-left cell. The range and the destination may overlap, and the destination may be in another workbook, where formulas refer to that workbook's sheets and names. Merged cells, column widths and row heights are not copied. A range holding part of an array formula (Ctrl+Shift+Enter) or a formula SharpCell cannot read throws NotSupportedException; nothing changes.

Conditional formats

sheet.AddConditionalFormat("B2:B6", ConditionalFormat.CellValue(
    ConditionalOperator.GreaterThan, "=500", ConditionalStyle.LightRedFill));
sheet.AddConditionalFormat("B2:B6", ConditionalFormat.Top(1, new ConditionalStyle { Bold = true }));
workbook.Recalculate();

foreach (var cell in sheet.Range("B2:B6").Cells)
{
    if (cell.ConditionalFormats.Count > 0)                    // the rules that apply now
        Console.WriteLine($"{cell.Address}: {cell.DisplayStyle.Fill}");   // as Excel shows it
}

ConditionalFormat has a factory for each of Excel's rules: CellValue (greater than, between and the others), Expression (a formula true for the cell), Top and Bottom (items or percent), AboveAverage and BelowAverage, DuplicateValues and UniqueValues, ContainsText, NotContainsText, BeginsWith, EndsWith, Blanks, NoBlanks, Errors, NoErrors and DateOccurring. Formulas are written for the range's top-left cell, as in Excel's dialog, and relative references move from cell to cell. A ConditionalStyle sets only what the rule changes: font style and colour, fill, borders and number format; LightRedFill, YellowFill and GreenFill are Excel's presets. Rules added first win where several set the same thing, and StopIfTrue (rule with { StopIfTrue = true }) skips the rules below for cells it applies to.

SharpCell works out which cells a rule applies to as Excel does. Cell.ConditionalFormats lists the rules that apply to a cell, from the values of the last Recalculate (rule formulas are calculated when read), and Cell.DisplayStyle is the cell's style with them put over it, Excel's DisplayFormat. An empty cell compares as 0 or as empty text; top, bottom and average rules count only numbers, and duplicates compare text ignoring case and leave empty cells out.

Colour scales, data bars and icon sets are read from files and kept, and their cells are listed as applying to numbers, but SharpCell does not work out their colours and cannot add them. Rules move with their cells when rows, columns or cells are inserted or deleted, and CopyTo copies them. ClearConditionalFormats takes a range out of every rule, or removes them all.

Notes

sheet["B2"].Note = new Note("Checked against the invoice", "Ann");
sheet["B3"].Note = new Note("Shown all the time") { IsVisible = true };

foreach (var (cell, note) in sheet.Notes)
    Console.WriteLine($"{cell.Address}: {note.Text} ({note.Author})");

sheet["B3"].Note = null;   // removes it

A Note is what Excel's Insert Note puts on a cell (a comment, before Excel 365): a text, who wrote it, and whether it is shown all the time (IsVisible) or only while the pointer rests on the cell. Notes move with their cells when rows, columns or cells are inserted or deleted, and CopyTo copies them. Excel starts the text of a note it makes with the author's name and a colon; SharpCell reads that line as part of the text and writes only the text it is given.

XlsxReader reads the notes of a file and XlsxWriter saves them. A note that did not change keeps its formatted text and its box as they were; a new or changed one is written as plain text in a box beside its cell, where Excel puts a new note. Threaded comments, Excel's newer comments with replies, are not notes: a file's own are kept as they are, and SharpCell does not make them.

Defined names

foreach (DefinedName name in workbook.DefinedNames)
    lines.Add($"{name.Name} {name.Formula} {name.Scope?.Name ?? "(workbook)"}");   // TaxRate =0.2 (workbook)

Workbook.DefinedNames lists workbook-wide and sheet-scoped names in the order they were first defined, spelled as they were defined. It is a snapshot: names defined later are not in it.