Skip to content
Shiny Controls v1.0 - The Ultra Control Suite for .NET MAUI & BlazorO...M...G!

Spreadsheet

SpreadsheetView opens, renders and edits .xlsx workbooks. Both hosts drive the same controller and paint with the same SkiaSharp routine, so MAUI and Blazor are not two implementations kept in step by hand — they are literally the same renderer.

  • NuGet downloads for Shiny.Maui.Controls.Office
  • NuGet downloads for Shiny.Blazor.Controls.Office
Frameworks
.NET MAUI
Blazor

The package is split so that almost none of it is host-specific. Shiny.Controls.Office.Shared owns the OOXML package, the sheet model, a transactional undo stack, the grid layout maths, the interaction logic and the formula engine — with no UI dependency at all, which is why it is covered by several hundred unit tests that never open a window. Shiny.Controls.Office.Skia turns that state into pixels. The two host packages contribute only a Skia surface, raw input forwarding, and a real text box for in-cell editing so the platform’s own keyboard and IME do the typing.

Edits are applied surgically to the open package. The workbook is opened once and held; changes go into the live XML DOM. Nothing is ever reconstructed from a parsed model, so parts the editor does not understand — macros, tracked changes, custom XML, pivot caches, conditional formatting, charts, embedded objects — survive because they are never read in the first place.

Two consequences worth relying on:

  • Opening a workbook and saving it without an edit produces a byte-identical file.
  • Editing one cell rewrites only the sheet, shared strings, workbook and styles parts. Everything else comes back byte-for-byte.

MAUI needs SkiaSharp registered, or the canvas never renders:

builder
.UseMauiApp<App>()
.UseShinyControls()
.UseSkiaSharp();
using var workbook = await Workbook.OpenAsync("/path/to/book.xlsx");
using var workbook = await Workbook.OpenAsync(stream);
using var workbook = Workbook.Create("Sheet1"); // start empty

Workbook is IDisposable and holds the package open — dispose it with the page.

<office:SpreadsheetView x:Name="Sheet"
Workbook="{Binding Workbook}"
SheetName="Budget"
CellChanged="OnCellChanged" />
<div style="height:420px">
<SpreadsheetView Workbook="workbook"
Theme="SpreadsheetTheme.Dark"
CellChanged="OnCellChanged" />
</div>

Every edit goes through the undo stack; never mutate cells directly.

workbook.Execute(new SetCellValueCommand("Budget", CellRef.Parse("B2"), CellValue.FromNumber(42)));
workbook.Execute(new SetCellFormulaCommand("Budget", CellRef.Parse("D2"), "B2*C2"));
workbook.Execute(new ClearRangeCommand("Budget", CellRange.Parse("A1:C3")));
workbook.Undo.Undo();
workbook.Undo.Redo();

A range clear is one undo step, not one per cell, and undoing over a cell that held a formula restores the formula — not the value it happened to be showing.

The engine indexes formulas lazily on the first edit or the first read of a calculated value, then recalculates incrementally in dependency order.

workbook.GetEffectiveValue("Budget", CellRef.Parse("D5")); // computed result
workbook.Evaluate("SUM(A1:A9)", "Budget", CellRef.Parse("Z1")); // ad-hoc, not stored
workbook.Calc.CircularCells; // non-empty on a circular reference

Around 80 functions ship across math, statistics, logic, text, lookup, date and information categories. An unknown function evaluates to #NAME? rather than throwing, and a circular reference is reported and left at zero rather than recursing until the stack dies.

await workbook.SaveAsync(); // over the path it was opened from
await workbook.SaveAsAsync("/new/path.xlsx");
await workbook.SaveToAsync(stream);
var bytes = workbook.ToArray();

Saving writes atomically — to a sibling temporary file, then a move — so an interrupted save never leaves a half-written document. It also refreshes the cached result of every formula (readers other than Excel show that cached value, so leaving it stale means the file displays wrong numbers) and sets fullCalcOnLoad so Excel re-verifies on open.

var collector = new UnsupportedFeatureCollector();
using var workbook = await Workbook.OpenAsync(path, collector);
foreach (var feature in collector.Features)
Console.WriteLine($"{feature.Part}: {feature.Feature} ({feature.Severity})");

Severities are NotRendered (preserved, not shown), NotEditable (preserved, shown, but edits nearby may not behave) and Lossy (cannot be preserved). Nothing currently reports Lossy, and a document that would should not be saved over its original without asking.

Both hosts expose the same controller, so a toolbar or formula bar drives identical state:

var controller = view.Controller;
controller.Selection.Active; // CellRef
controller.ActiveCellText; // what a formula bar should show
controller.BeginEdit();
controller.Move(MoveDirection.Down, extend: false, toEdge: true); // Ctrl+Down
controller.ClearSelection();
controller.Undo();
  • Inserting and deleting rows and columns. Deliberately deferred: it is the hardest edit in the format, because references must be rewritten across formulas, merged cells, conditional formatting, defined names, data validation, charts and tables.
  • Adding or removing merged cells — existing merges render but cannot be changed.
  • Editing charts, pivot tables or conditional formatting.
  • Multi-range (Ctrl-click) selection, copy/paste, and drag-to-fill (the fill handle is drawn but inert).
  • Physical-key navigation on MAUI. MAUI has no portable key-down event, so arrow keys work on Blazor only; on MAUI call Move, BeginEdit and ClearSelection from your own platform key hook.
MAUI Blazor
Spreadsheet on iOS, with the formula bar and a selected cell Spreadsheet on Blazor