diff --git a/Radzen.Blazor.Tests/Spreadsheet/PasteFidelityTests.cs b/Radzen.Blazor.Tests/Spreadsheet/PasteFidelityTests.cs
new file mode 100644
index 000000000..83add36e8
--- /dev/null
+++ b/Radzen.Blazor.Tests/Spreadsheet/PasteFidelityTests.cs
@@ -0,0 +1,374 @@
+using Xunit;
+using Radzen.Documents.Spreadsheet;
+
+namespace Radzen.Blazor.Spreadsheet.Tests;
+
+#nullable enable
+
+// Excel-parity cell transfer: paste, cut and autofill carry the full cell state
+// (value, formula, format, quote prefix, hyperlink) and undo/redo restore it symmetrically.
+public class PasteFidelityTests
+{
+ private static (Worksheet sheet, SpreadsheetClipboard clipboard, UndoRedoStack stack) Setup()
+ {
+ var sheet = new Worksheet(20, 5);
+ return (sheet, new SpreadsheetClipboard(), new UndoRedoStack(sheet));
+ }
+
+ private static string CopyRange(Worksheet sheet, SpreadsheetClipboard clipboard, string range)
+ {
+ sheet.Selection.Select(RangeRef.Parse(range));
+ clipboard.Copy(sheet);
+ return sheet.GetDelimitedString(RangeRef.Parse(range));
+ }
+
+ private static string CutRange(Worksheet sheet, SpreadsheetClipboard clipboard, string range)
+ {
+ sheet.Selection.Select(RangeRef.Parse(range));
+ clipboard.Cut(sheet);
+ return sheet.GetDelimitedString(RangeRef.Parse(range));
+ }
+
+ [Fact]
+ public void Paste_CopiesFormatQuotePrefixAndHyperlink()
+ {
+ var (sheet, clipboard, stack) = Setup();
+ var src = sheet.Cells[0, 0];
+ src.SetValue("'0123");
+ src.Format.Bold = true;
+ src.Format.BackgroundColor = "#FF0000";
+ src.Hyperlink = new Hyperlink { Url = "https://radzen.com", Text = "Radzen" };
+
+ var text = CopyRange(sheet, clipboard, "A1:A1");
+ stack.Execute(new PasteCommand(clipboard, sheet, RangeRef.Parse("C1:C1"), text));
+
+ var dst = sheet.Cells[0, 2];
+ Assert.Equal("0123", dst.Value);
+ Assert.True(dst.QuotePrefix);
+ Assert.True(dst.FormatOrNull?.Bold);
+ Assert.Equal("#FF0000", dst.FormatOrNull?.BackgroundColor);
+ Assert.Equal("https://radzen.com", dst.Hyperlink?.Url);
+ }
+
+ [Fact]
+ public void Paste_ValueOverFormulaCell_ClearsFormula()
+ {
+ var (sheet, clipboard, stack) = Setup();
+ sheet.Cells[0, 0].Value = 5d;
+ sheet.Cells[0, 2].Formula = "=A1*2";
+
+ var text = CopyRange(sheet, clipboard, "A1:A1");
+ stack.Execute(new PasteCommand(clipboard, sheet, RangeRef.Parse("C1:C1"), text));
+
+ Assert.Null(sheet.Cells[0, 2].Formula);
+ Assert.Equal(5d, sheet.Cells[0, 2].Value);
+
+ sheet.Cells[0, 0].Value = 9d; // recalc must not overwrite the pasted value
+ Assert.Equal(5d, sheet.Cells[0, 2].Value);
+ }
+
+ [Fact]
+ public void Paste_UnformattedSource_ClearsDestinationFormat()
+ {
+ var (sheet, clipboard, stack) = Setup();
+ sheet.Cells[0, 0].Value = "plain";
+ sheet.Cells[0, 2].Value = "styled";
+ sheet.Cells[0, 2].Format.Bold = true;
+
+ var text = CopyRange(sheet, clipboard, "A1:A1");
+ stack.Execute(new PasteCommand(clipboard, sheet, RangeRef.Parse("C1:C1"), text));
+
+ Assert.Equal("plain", sheet.Cells[0, 2].Value);
+ Assert.Null(sheet.Cells[0, 2].FormatOrNull);
+ }
+
+ [Fact]
+ public void Paste_UnpopulatedSourceCell_BlanksPopulatedDestination()
+ {
+ var (sheet, clipboard, stack) = Setup();
+ sheet.Cells[0, 0].Value = "A";
+ // A2 stays unpopulated
+ sheet.Cells[0, 2].Value = "old";
+ sheet.Cells[1, 2].Value = "stale";
+ sheet.Cells[1, 2].Format.Bold = true;
+ sheet.Cells[1, 2].Hyperlink = new Hyperlink { Url = "https://x" };
+
+ var text = CopyRange(sheet, clipboard, "A1:A2");
+ stack.Execute(new PasteCommand(clipboard, sheet, RangeRef.Parse("C1:C1"), text));
+
+ Assert.Equal("A", sheet.Cells[0, 2].Value);
+ Assert.Null(sheet.Cells[1, 2].Value);
+ Assert.Null(sheet.Cells[1, 2].FormatOrNull);
+ Assert.Null(sheet.Cells[1, 2].Hyperlink);
+ }
+
+ [Fact]
+ public void Paste_UnpopulatedSourceOverUnpopulatedDestination_DoesNotMaterialize()
+ {
+ var (sheet, clipboard, stack) = Setup();
+ sheet.Cells[0, 0].Value = "A";
+
+ var text = CopyRange(sheet, clipboard, "A1:A2");
+ stack.Execute(new PasteCommand(clipboard, sheet, RangeRef.Parse("C1:C1"), text));
+
+ Assert.False(sheet.Cells.TryGet(1, 2, out _));
+ }
+
+ [Fact]
+ public void TiledPaste_CopiesFormatsPerTile()
+ {
+ var (sheet, clipboard, stack) = Setup();
+ sheet.Cells[0, 0].Value = "X";
+ sheet.Cells[0, 0].Format.Bold = true;
+
+ var text = CopyRange(sheet, clipboard, "A1:A1");
+ stack.Execute(new PasteCommand(clipboard, sheet, RangeRef.Parse("C1:C3"), text));
+
+ for (var row = 0; row <= 2; row++)
+ {
+ Assert.Equal("X", sheet.Cells[row, 2].Value);
+ Assert.True(sheet.Cells[row, 2].FormatOrNull?.Bold);
+ }
+ }
+
+ [Fact]
+ public void Cut_ClearsSourceAndTransfersEverything()
+ {
+ var (sheet, clipboard, stack) = Setup();
+ var src = sheet.Cells[0, 0];
+ src.SetValue("'007");
+ src.Format.Bold = true;
+ src.Hyperlink = new Hyperlink { Url = "https://radzen.com" };
+
+ var text = CutRange(sheet, clipboard, "A1:A1");
+ stack.Execute(new PasteCommand(clipboard, sheet, RangeRef.Parse("C1:C1"), text));
+
+ Assert.Null(src.Value);
+ Assert.Null(src.FormatOrNull);
+ Assert.False(src.QuotePrefix);
+ Assert.Null(src.Hyperlink);
+
+ var dst = sheet.Cells[0, 2];
+ Assert.Equal("007", dst.Value);
+ Assert.True(dst.QuotePrefix);
+ Assert.True(dst.FormatOrNull?.Bold);
+ Assert.Equal("https://radzen.com", dst.Hyperlink?.Url);
+ }
+
+ [Fact]
+ public void SetValue_PlainValueOnFormulaCell_ClearsFormula()
+ {
+ var sheet = new Worksheet(20, 5);
+ sheet.Cells[0, 0].Value = 2d;
+ sheet.Cells[0, 1].Formula = "=A1*3";
+ Assert.Equal(6d, sheet.Cells[0, 1].Value);
+
+ sheet.Cells[0, 1].SetValue("42");
+
+ Assert.Null(sheet.Cells[0, 1].Formula);
+ Assert.Equal(42d, sheet.Cells[0, 1].Value);
+
+ sheet.Cells[0, 0].Value = 10d; // recalc must not resurrect the formula
+ Assert.Equal(42d, sheet.Cells[0, 1].Value);
+ }
+
+ [Fact]
+ public void SetValue_QuotePrefix_StoresLiteralTextWithoutTypeInference()
+ {
+ var sheet = new Worksheet(20, 5);
+ sheet.Cells[0, 0].SetValue("'0123");
+
+ Assert.Equal("0123", sheet.Cells[0, 0].Value);
+ Assert.Equal(CellDataType.String, sheet.Cells[0, 0].ValueType);
+ Assert.True(sheet.Cells[0, 0].QuotePrefix);
+ Assert.Equal("'0123", sheet.Cells[0, 0].GetValue());
+ }
+
+ [Fact]
+ public void TypedString_SurvivesCaptureAndUndo()
+ {
+ var (sheet, clipboard, stack) = Setup();
+ sheet.Cells[0, 2].Data = CellData.FromString("0123"); // explicitly String-typed, like CsvReader with ParseValues=false
+ sheet.Cells[0, 0].Value = "other";
+
+ var text = CopyRange(sheet, clipboard, "A1:A1");
+ stack.Execute(new PasteCommand(clipboard, sheet, RangeRef.Parse("C1:C1"), text));
+ stack.Undo();
+
+ Assert.Equal("0123", sheet.Cells[0, 2].Value);
+ Assert.Equal(CellDataType.String, sheet.Cells[0, 2].ValueType);
+ }
+
+ [Fact]
+ public void Undo_PasteFormulaOverEmpty_LeavesCellEmpty()
+ {
+ var (sheet, clipboard, stack) = Setup();
+ sheet.Cells[0, 0].Value = 1d;
+ sheet.Cells[1, 0].Value = 2d;
+ sheet.Cells[2, 0].Formula = "=SUM(A1:A2)";
+
+ var text = CopyRange(sheet, clipboard, "A3:A3");
+ stack.Execute(new PasteCommand(clipboard, sheet, RangeRef.Parse("C3:C3"), text));
+ Assert.Equal("=SUM(C1:C2)", sheet.Cells[2, 2].Formula);
+
+ stack.Undo();
+
+ Assert.Null(sheet.Cells[2, 2].Formula);
+ Assert.Null(sheet.Cells[2, 2].Value);
+ }
+
+ [Fact]
+ public void Undo_PasteFormat_RestoresPriorState()
+ {
+ var (sheet, clipboard, stack) = Setup();
+ sheet.Cells[0, 0].Value = "X";
+ sheet.Cells[0, 0].Format.Bold = true;
+ sheet.Cells[0, 2].Value = "plain"; // populated, unformatted
+
+ var text = CopyRange(sheet, clipboard, "A1:A2");
+ stack.Execute(new PasteCommand(clipboard, sheet, RangeRef.Parse("C1:C1"), text));
+ Assert.True(sheet.Cells[0, 2].FormatOrNull?.Bold);
+
+ stack.Undo();
+
+ Assert.Equal("plain", sheet.Cells[0, 2].Value);
+ Assert.Null(sheet.Cells[0, 2].FormatOrNull);
+ Assert.False(sheet.Cells.TryGet(1, 2, out _)); // unpopulated destination stays unmaterialized
+ }
+
+ [Fact]
+ public void RedoUndo_SnapshotIsolatedFromLaterEdits()
+ {
+ var (sheet, clipboard, stack) = Setup();
+ sheet.Cells[0, 0].Value = "X";
+ sheet.Cells[0, 0].Format.Bold = true;
+ sheet.Cells[0, 2].Value = "orig";
+ sheet.Cells[0, 2].Format.Italic = true;
+
+ var text = CopyRange(sheet, clipboard, "A1:A1");
+ stack.Execute(new PasteCommand(clipboard, sheet, RangeRef.Parse("C1:C1"), text));
+ stack.Undo();
+
+ sheet.Cells[0, 2].Format.Underline = true; // must not leak into any snapshot
+
+ stack.Redo();
+ Assert.True(sheet.Cells[0, 2].FormatOrNull?.Bold);
+ Assert.False(sheet.Cells[0, 2].FormatOrNull?.Underline);
+
+ stack.Undo();
+ Assert.True(sheet.Cells[0, 2].FormatOrNull?.Italic);
+ Assert.False(sheet.Cells[0, 2].FormatOrNull?.Bold);
+ }
+
+ [Fact]
+ public void CutUndoRedo_RestoresAndReclearsSource()
+ {
+ var (sheet, clipboard, stack) = Setup();
+ var src = sheet.Cells[0, 0];
+ src.SetValue("'A");
+ src.Format.Bold = true;
+ src.Hyperlink = new Hyperlink { Url = "https://x" };
+
+ var text = CutRange(sheet, clipboard, "A1:A1");
+ stack.Execute(new PasteCommand(clipboard, sheet, RangeRef.Parse("C1:C1"), text));
+
+ stack.Undo();
+ Assert.Equal("A", src.Value);
+ Assert.True(src.QuotePrefix);
+ Assert.True(src.FormatOrNull?.Bold);
+ Assert.Equal("https://x", src.Hyperlink?.Url);
+
+ stack.Redo();
+ Assert.Null(src.Value);
+ Assert.Null(src.FormatOrNull);
+ Assert.Null(src.Hyperlink);
+ Assert.Equal("A", sheet.Cells[0, 2].Value);
+ }
+
+ [Fact]
+ public void MergeUndo_OnUnpopulatedTopLeft_LeavesNoFormat()
+ {
+ var sheet = new Worksheet(20, 5);
+ var stack = new UndoRedoStack(sheet);
+
+ stack.Execute(new MergeCellsCommand(sheet, RangeRef.Parse("C1:D2"), center: true));
+ Assert.Equal(TextAlign.Center, sheet.Cells[0, 2].FormatOrNull?.TextAlign);
+
+ stack.Undo();
+
+ Assert.False(sheet.Cells.TryGet(0, 2, out var cell) && cell.FormatOrNull is not null);
+ }
+
+ [Fact]
+ public void SortUndo_RestoresQuotePrefixHyperlinkAndEmptiness()
+ {
+ var sheet = new Worksheet(20, 5);
+ var stack = new UndoRedoStack(sheet);
+ sheet.Cells[0, 0].SetValue("'B");
+ sheet.Cells[0, 0].Hyperlink = new Hyperlink { Url = "https://b" };
+ sheet.Cells[1, 0].SetValue("'A");
+ // A3 unpopulated inside the sort range
+
+ stack.Execute(new SortCommand(sheet, RangeRef.Parse("A1:A3"), SortOrder.Ascending, 0));
+ Assert.Equal("A", sheet.Cells[0, 0].Value);
+
+ stack.Undo();
+
+ Assert.Equal("B", sheet.Cells[0, 0].Value);
+ Assert.True(sheet.Cells[0, 0].QuotePrefix);
+ Assert.Equal("https://b", sheet.Cells[0, 0].Hyperlink?.Url);
+ Assert.True(!sheet.Cells.TryGet(2, 0, out var third) || third.IsEmpty);
+ }
+
+ [Fact]
+ public void Autofill_FromUnformattedAndUnpopulatedSources_ClearsDestinationFormat()
+ {
+ var sheet = new Worksheet(20, 5);
+ sheet.Cells[0, 0].Value = "plain"; // populated, unformatted
+ // A2 unpopulated
+ sheet.Cells[2, 0].Value = "old";
+ sheet.Cells[2, 0].Format.Bold = true;
+ sheet.Cells[3, 0].Value = "older";
+ sheet.Cells[3, 0].Format.Bold = true;
+
+ new AutofillCommand(sheet, RangeRef.Parse("A1:A2"), RangeRef.Parse("A1:A4"), AutofillDirection.Down).Execute();
+
+ Assert.Null(sheet.Cells[2, 0].FormatOrNull);
+ Assert.Null(sheet.Cells[3, 0].FormatOrNull);
+ }
+
+ [Fact]
+ public void AutofillUndo_RemovesCopiedFormats()
+ {
+ var sheet = new Worksheet(20, 5);
+ var stack = new UndoRedoStack(sheet);
+ sheet.Cells[0, 0].Value = 1d;
+ sheet.Cells[0, 0].Format.Bold = true;
+
+ stack.Execute(new AutofillCommand(sheet, RangeRef.Parse("A1:A1"), RangeRef.Parse("A1:A3"), AutofillDirection.Down));
+ Assert.True(sheet.Cells[2, 0].FormatOrNull?.Bold);
+
+ stack.Undo();
+
+ Assert.True(!sheet.Cells.TryGet(2, 0, out var cell) || cell.FormatOrNull is null);
+ }
+
+ [Fact]
+ public void UnbatchedUnexecute_RecalculatesDependents()
+ {
+ var (sheet, clipboard, _) = Setup();
+ sheet.Cells[0, 0].Value = 7d;
+ sheet.Cells[0, 2].Value = 1d;
+ sheet.Cells[5, 0].Formula = "=C1*10";
+
+ var text = CopyRange(sheet, clipboard, "A1:A1");
+ var command = new PasteCommand(clipboard, sheet, RangeRef.Parse("C1:C1"), text);
+ command.Execute();
+ Assert.Equal(70d, sheet.Cells[5, 0].Value);
+
+ command.Unexecute(); // direct call, no UndoRedoStack batch
+
+ Assert.Equal(1d, sheet.Cells[0, 2].Value);
+ Assert.Equal(10d, sheet.Cells[5, 0].Value);
+ }
+}
diff --git a/Radzen.Blazor/Documents/Spreadsheet/Cell.cs b/Radzen.Blazor/Documents/Spreadsheet/Cell.cs
index c5306b335..9e9a90609 100644
--- a/Radzen.Blazor/Documents/Spreadsheet/Cell.cs
+++ b/Radzen.Blazor/Documents/Spreadsheet/Cell.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
namespace Radzen.Documents.Spreadsheet;
@@ -18,8 +19,9 @@ public class Cell
private Format? format;
///
- /// Gets or sets the format of the cell.
+ /// Gets or sets the format of the cell. Setting null clears the format.
///
+ [AllowNull]
public Format Format
{
get
@@ -56,7 +58,8 @@ public class Cell
{
var clone = new Cell(Worksheet, Address)
{
- Data = new CellData(Value),
+ // Share Data: it is immutable, and rebuilding from Value re-infers the type (corrupts "0123").
+ Data = Data,
QuotePrefix = QuotePrefix,
Hyperlink = Hyperlink?.Clone(),
};
@@ -65,22 +68,24 @@ public class Cell
// A clone is a detached snapshot/transport copy and must stay out of the graph.
clone.formula = formula;
clone.FormulaSyntaxTree = FormulaSyntaxTree;
- if (format is not null)
- {
- clone.format = format.Clone();
- clone.format.Changed += clone.OnFormatChanged;
- }
+ // Not subscribed to: CopyFrom aliases the format into a live cell, and a stale
+ // subscription would root the discarded clone for the format's lifetime.
+ clone.format = format?.Clone();
return clone;
}
///
/// Copies the properties from another cell to this cell.
///
- public void CopyFrom(Cell other)
+ public void CopyFrom(Cell other) => CopyFrom(other, null);
+
+ // The formula override replaces the source formula in the same setter call; adjusting the
+ // formula on a detached clone instead would register the clone in the live dependency graph.
+ internal void CopyFrom(Cell other, string? formulaOverride)
{
ArgumentNullException.ThrowIfNull(other);
Data = other.Data;
- Formula = other.Formula;
+ Formula = formulaOverride ?? other.Formula;
QuotePrefix = other.QuotePrefix;
format?.Changed -= OnFormatChanged;
@@ -94,6 +99,17 @@ public class Cell
Changed?.Invoke(this);
}
+ // Excel's full clear: contents, format, quote prefix and hyperlink. Clear Contents
+ // (which keeps formats) belongs to ClearContentsCommand instead.
+ internal void Clear()
+ {
+ Formula = null;
+ Value = null;
+ Format = null;
+ Hyperlink = null;
+ OnChanged();
+ }
+
internal Format? FormatOrNull => format;
///
@@ -191,12 +207,12 @@ public class Cell
{
if (value is not null && value.StartsWith('\''))
{
- if (Formula is not null)
- {
- Formula = null;
- }
- Value = value[1..];
+ Formula = null;
+ // Bypass the Value setter: the quote prefix means literal text, so the
+ // string must not go through type inference ('0123 stays "0123").
+ Data = CellData.FromString(value[1..]);
QuotePrefix = true;
+ Worksheet.OnCellValueChanged(this);
}
else if (value?.StartsWith('=') == true && value != "=")
{
@@ -204,6 +220,7 @@ public class Cell
}
else
{
+ Formula = null;
Value = value;
}
}
diff --git a/Radzen.Blazor/Documents/Spreadsheet/Worksheet.cs b/Radzen.Blazor/Documents/Spreadsheet/Worksheet.cs
index a843caae1..56bb4db02 100644
--- a/Radzen.Blazor/Documents/Spreadsheet/Worksheet.cs
+++ b/Radzen.Blazor/Documents/Spreadsheet/Worksheet.cs
@@ -726,37 +726,28 @@ public partial class Worksheet
var dr = sr + rowDelta;
var dc = sc + colDelta;
- if (!sourceSheet.Cells.TryGet(sr, sc, out var srcCell))
- {
- continue;
- }
-
if (dr < 0 || dr >= RowCount || dc < 0 || dc >= ColumnCount)
{
continue;
}
+ if (!sourceSheet.Cells.TryGet(sr, sc, out var srcCell))
+ {
+ // Empty source cells blank the destination, like Excel.
+ if (Cells.TryGet(dr, dc, out var existing))
+ {
+ existing.Clear();
+ cells.Add(existing);
+ }
+ continue;
+ }
+
+ var adjusted = !string.IsNullOrEmpty(srcCell.Formula) && adjustment != FormulaAdjustment.Preserve
+ ? AdjustFormulaForCopy(srcCell.Formula!, rowDelta, colDelta)
+ : null;
+
var dstCell = Cells[dr, dc];
-
- if (!string.IsNullOrEmpty(srcCell.Formula))
- {
- var formula = srcCell.Formula!;
-
- if (adjustment == FormulaAdjustment.Preserve)
- {
- dstCell.Formula = formula;
- }
- else
- {
- var adjusted = AdjustFormulaForCopy(formula, rowDelta, colDelta);
- dstCell.Formula = adjusted;
- }
- }
- else
- {
- dstCell.Value = srcCell.Value;
- }
-
+ dstCell.CopyFrom(srcCell.Clone(), adjusted);
cells.Add(dstCell);
}
}
diff --git a/Radzen.Blazor/Spreadsheet/AutofillCommand.cs b/Radzen.Blazor/Spreadsheet/AutofillCommand.cs
index bf93026e5..ce9d5b24e 100644
--- a/Radzen.Blazor/Spreadsheet/AutofillCommand.cs
+++ b/Radzen.Blazor/Spreadsheet/AutofillCommand.cs
@@ -195,7 +195,7 @@ class AutofillCommand : RangeSnapshotCommandBase
{
sourceValues.Add(srcCell.Value);
sourceFormulas.Add(srcCell.Formula);
- sourceFormats.Add(srcCell.Format?.Clone());
+ sourceFormats.Add(srcCell.FormatOrNull?.Clone());
}
else
{
@@ -217,10 +217,7 @@ class AutofillCommand : RangeSnapshotCommandBase
var dstCell = sheet.Cells[row, column];
- if (sourceFormats[sourceIndex] is not null)
- {
- dstCell.Format = sourceFormats[sourceIndex]!.Clone();
- }
+ dstCell.Format = sourceFormats[sourceIndex]?.Clone();
if (!string.IsNullOrEmpty(sourceFormulas[sourceIndex]))
{
@@ -273,7 +270,7 @@ class AutofillCommand : RangeSnapshotCommandBase
{
sourceValues.Add(srcCell.Value);
sourceFormulas.Add(srcCell.Formula);
- sourceFormats.Add(srcCell.Format?.Clone());
+ sourceFormats.Add(srcCell.FormatOrNull?.Clone());
}
else
{
@@ -295,10 +292,7 @@ class AutofillCommand : RangeSnapshotCommandBase
var dstCell = sheet.Cells[row, column];
- if (sourceFormats[sourceIndex] is not null)
- {
- dstCell.Format = sourceFormats[sourceIndex]!.Clone();
- }
+ dstCell.Format = sourceFormats[sourceIndex]?.Clone();
if (!string.IsNullOrEmpty(sourceFormulas[sourceIndex]))
{
diff --git a/Radzen.Blazor/Spreadsheet/ClearContentsCommand.cs b/Radzen.Blazor/Spreadsheet/ClearContentsCommand.cs
index 6c375abcc..ba718ba22 100644
--- a/Radzen.Blazor/Spreadsheet/ClearContentsCommand.cs
+++ b/Radzen.Blazor/Spreadsheet/ClearContentsCommand.cs
@@ -27,6 +27,7 @@ class ClearContentsCommand : RangeSnapshotCommandBase
{
Capture(new CellRef(row, column));
+ // Deliberately not Cell.Clear: Excel's Clear Contents keeps formats.
cell.Formula = null;
cell.Value = null;
}
diff --git a/Radzen.Blazor/Spreadsheet/PasteCommand.cs b/Radzen.Blazor/Spreadsheet/PasteCommand.cs
index 0881ba450..2b66565a5 100644
--- a/Radzen.Blazor/Spreadsheet/PasteCommand.cs
+++ b/Radzen.Blazor/Spreadsheet/PasteCommand.cs
@@ -1,6 +1,4 @@
-using System;
using System.Collections.Generic;
-using System.Linq;
using Radzen.Documents.Spreadsheet;
namespace Radzen.Blazor.Spreadsheet;
@@ -15,7 +13,7 @@ class PasteCommand : RangeSnapshotCommandBase
private readonly SpreadsheetClipboard clipboard;
private readonly RangeRef destination;
private readonly string? text;
- private readonly Dictionary result = [];
+ private readonly Dictionary result = [];
private bool pasted;
public override SheetAction RequiredAction => SheetAction.EditCell;
@@ -32,11 +30,18 @@ class PasteCommand : RangeSnapshotCommandBase
{
if (pasted)
{
- // Redo: replay the captured result instead of re-running the (possibly consumed) clipboard.
+ // Redo: replay the captured result instead of re-running the (possibly consumed)
+ // clipboard. Execute cleared the snapshot, so re-capture it first or the next
+ // undo would have nothing to restore.
+ foreach (var cellRef in result.Keys)
+ {
+ Capture(cellRef);
+ }
+
Restore(result);
return true;
}
-
+
var destinationRange = clipboard.GetPasteRange(sheet, destination, text);
if (destinationRange == RangeRef.Invalid)
@@ -64,25 +69,14 @@ class PasteCommand : RangeSnapshotCommandBase
pasted = true;
return true;
}
-
+
public override void Unexecute() => RestoreSnapshot();
private void CaptureResult()
{
foreach (var cellRef in snapshot.Keys)
{
- string? formula = null;
- object? value = null;
- Format? format = null;
-
- if (sheet.Cells.TryGet(cellRef.Row, cellRef.Column, out var cell))
- {
- value = cell.Value;
- formula = cell.Formula;
- format = cell.Format?.Clone();
- }
-
- result[cellRef] = (value, formula, format);
+ result[cellRef] = sheet.Cells.TryGet(cellRef.Row, cellRef.Column, out var cell) ? cell.Clone() : null;
}
}
-}
\ No newline at end of file
+}
diff --git a/Radzen.Blazor/Spreadsheet/RangeSnapshotCommandBase.cs b/Radzen.Blazor/Spreadsheet/RangeSnapshotCommandBase.cs
index d9d9669d8..f2e039069 100644
--- a/Radzen.Blazor/Spreadsheet/RangeSnapshotCommandBase.cs
+++ b/Radzen.Blazor/Spreadsheet/RangeSnapshotCommandBase.cs
@@ -7,7 +7,7 @@ namespace Radzen.Blazor.Spreadsheet;
#nullable enable
///
-/// Base class for commands that snapshot a set of cells (value, formula, format) for undo support.
+/// Base class for commands that snapshot a set of cells as detached clones for undo support.
///
public abstract class RangeSnapshotCommandBase : ICommand, IProtectedCommand
{
@@ -23,9 +23,10 @@ public abstract class RangeSnapshotCommandBase : ICommand, IProtectedCommand
protected readonly Worksheet sheet;
///
- /// The captured cell snapshots keyed by cell reference.
+ /// The captured cell snapshots keyed by cell reference. A snapshot is a detached clone,
+ /// or null when the cell did not exist at capture time.
///
- protected readonly Dictionary snapshot = [];
+ protected readonly Dictionary snapshot = [];
///
/// Initializes a new instance of the class.
@@ -36,22 +37,11 @@ public abstract class RangeSnapshotCommandBase : ICommand, IProtectedCommand
}
///
- /// Captures the current value, formula and format of the cell at into the snapshot.
+ /// Captures the cell at into the snapshot as a detached clone.
///
protected void Capture(CellRef cellRef)
{
- object? value = null;
- string? formula = null;
- Format? format = null;
-
- if (sheet.Cells.TryGet(cellRef.Row, cellRef.Column, out var cell))
- {
- value = cell.Value;
- formula = cell.Formula;
- format = cell.Format?.Clone();
- }
-
- snapshot[cellRef] = (value, formula, format);
+ snapshot[cellRef] = sheet.Cells.TryGet(cellRef.Row, cellRef.Column, out var cell) ? cell.Clone() : null;
}
///
@@ -68,37 +58,38 @@ public abstract class RangeSnapshotCommandBase : ICommand, IProtectedCommand
}
///
- /// Restores the captured cells back into the sheet using the formula-or-value convention.
+ /// Restores the captured cells back into the sheet.
///
protected void RestoreSnapshot() => Restore(snapshot);
///
- /// Writes the captured (value/formula/format) state of back into the sheet.
+ /// Writes the captured cell state back into the sheet. Cells that did not exist at capture
+ /// time are cleared. Batched so dependent formulas recalculate once, even when called
+ /// outside the undo stack.
///
- protected void Restore(IReadOnlyDictionary cells)
+ protected void Restore(IReadOnlyDictionary cells)
{
ArgumentNullException.ThrowIfNull(cells);
- foreach (var entry in cells)
+ sheet.Batch(() =>
{
- var cellRef = entry.Key;
- var (value, formula, format) = entry.Value;
- var cell = sheet.Cells[cellRef.Row, cellRef.Column];
-
- if (formula is not null)
+ foreach (var (cellRef, snap) in cells)
{
- cell.Formula = formula;
+ if (snap is null)
+ {
+ if (sheet.Cells.TryGet(cellRef.Row, cellRef.Column, out var cell))
+ {
+ cell.Clear();
+ }
+ }
+ else
+ {
+ // Clone again: CopyFrom aliases the format, and the snapshot must stay
+ // isolated from later edits of the live cell.
+ sheet.Cells[cellRef.Row, cellRef.Column].CopyFrom(snap.Clone());
+ }
}
- else
- {
- cell.Value = value;
- }
-
- if (format is not null)
- {
- cell.Format = format;
- }
- }
+ });
}
///
diff --git a/Radzen.Blazor/Spreadsheet/SpreadsheetClipboard.cs b/Radzen.Blazor/Spreadsheet/SpreadsheetClipboard.cs
index c6e78a448..9a4021afd 100644
--- a/Radzen.Blazor/Spreadsheet/SpreadsheetClipboard.cs
+++ b/Radzen.Blazor/Spreadsheet/SpreadsheetClipboard.cs
@@ -159,8 +159,7 @@ class SpreadsheetClipboard
{
if (sourceSheet.Cells.TryGet(sr, sc, out var srcCell))
{
- srcCell.Formula = null;
- srcCell.Value = null;
+ srcCell.Clear();
}
}
}