mirror of
https://github.com/radzenhq/radzen-blazor.git
synced 2026-04-06 14:21:09 +00:00
Add SheetView and WorkbookView for per-sheet UI state
SheetView wraps a Sheet with per-sheet UndoRedoStack and rendering method delegation. WorkbookView manages SheetView instances per sheet via lazy creation, enabling per-sheet undo history (like Excel) and future multi-sheet support. Purely additive — no existing code changes.
This commit is contained in:
committed by
Vladimir Enchev
parent
02aef6c59f
commit
4450a068e3
309
Radzen.Blazor.Tests/Spreadsheet/SheetViewTests.cs
Normal file
309
Radzen.Blazor.Tests/Spreadsheet/SheetViewTests.cs
Normal file
@@ -0,0 +1,309 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class SheetViewTests
|
||||
{
|
||||
// --- Construction ---
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SetsSheet()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
var view = new SheetView(sheet);
|
||||
|
||||
Assert.Same(sheet, view.Sheet);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ThrowsOnNull()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new SheetView(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CreatesCommands()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
var view = new SheetView(sheet);
|
||||
|
||||
Assert.NotNull(view.Commands);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultOffsets()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
var view = new SheetView(sheet);
|
||||
|
||||
Assert.Equal(24, view.RowHeaderOffset);
|
||||
Assert.Equal(100, view.ColumnHeaderOffset);
|
||||
}
|
||||
|
||||
// --- Rendering method delegation ---
|
||||
|
||||
[Fact]
|
||||
public void GetRowRange_DelegatesToAxis()
|
||||
{
|
||||
var sheet = new Sheet(100, 10);
|
||||
var view = new SheetView(sheet);
|
||||
|
||||
var fromAxis = sheet.Rows.GetIndexRange(0, 500);
|
||||
var fromView = view.GetRowRange(0, 500);
|
||||
|
||||
Assert.Equal(fromAxis.Start, fromView.Start);
|
||||
Assert.Equal(fromAxis.End, fromView.End);
|
||||
Assert.Equal(fromAxis.Offset, fromView.Offset);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetColumnRange_DelegatesToAxis()
|
||||
{
|
||||
var sheet = new Sheet(10, 100);
|
||||
var view = new SheetView(sheet);
|
||||
|
||||
var fromAxis = sheet.Columns.GetIndexRange(0, 500);
|
||||
var fromView = view.GetColumnRange(0, 500);
|
||||
|
||||
Assert.Equal(fromAxis.Start, fromView.Start);
|
||||
Assert.Equal(fromAxis.End, fromView.End);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetRowPixelRange_DelegatesToAxis()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Rows[3] = 50;
|
||||
var view = new SheetView(sheet);
|
||||
|
||||
var fromAxis = sheet.Rows.GetPixelRange(2, 4);
|
||||
var fromView = view.GetRowPixelRange(2, 4);
|
||||
|
||||
Assert.Equal(fromAxis.Start, fromView.Start);
|
||||
Assert.Equal(fromAxis.End, fromView.End);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetRowPixelRange_SingleIndex_DelegatesToAxis()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
var view = new SheetView(sheet);
|
||||
|
||||
var fromAxis = sheet.Rows.GetPixelRange(5);
|
||||
var fromView = view.GetRowPixelRange(5);
|
||||
|
||||
Assert.Equal(fromAxis.Start, fromView.Start);
|
||||
Assert.Equal(fromAxis.End, fromView.End);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetColumnPixelRange_DelegatesToAxis()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
sheet.Columns[2] = 200;
|
||||
var view = new SheetView(sheet);
|
||||
|
||||
var fromAxis = sheet.Columns.GetPixelRange(1, 3);
|
||||
var fromView = view.GetColumnPixelRange(1, 3);
|
||||
|
||||
Assert.Equal(fromAxis.Start, fromView.Start);
|
||||
Assert.Equal(fromAxis.End, fromView.End);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetColumnPixelRange_SingleIndex_DelegatesToAxis()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
var view = new SheetView(sheet);
|
||||
|
||||
var fromAxis = sheet.Columns.GetPixelRange(5);
|
||||
var fromView = view.GetColumnPixelRange(5);
|
||||
|
||||
Assert.Equal(fromAxis.Start, fromView.Start);
|
||||
Assert.Equal(fromAxis.End, fromView.End);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TotalHeight_DelegatesToAxis()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
var view = new SheetView(sheet);
|
||||
|
||||
Assert.Equal(sheet.Rows.Total, view.TotalHeight);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TotalWidth_DelegatesToAxis()
|
||||
{
|
||||
var sheet = new Sheet(10, 10);
|
||||
var view = new SheetView(sheet);
|
||||
|
||||
Assert.Equal(sheet.Columns.Total, view.TotalWidth);
|
||||
}
|
||||
|
||||
// --- Per-sheet Commands ---
|
||||
|
||||
[Fact]
|
||||
public void Commands_ExecuteAndUndo()
|
||||
{
|
||||
var sheet = new Sheet(5, 5);
|
||||
var view = new SheetView(sheet);
|
||||
sheet.Cells[0, 0].Value = "A";
|
||||
|
||||
var cmd = new ClearContentsCommand(sheet, new RangeRef(new CellRef(0, 0), new CellRef(0, 0)));
|
||||
view.Commands.Execute(cmd);
|
||||
|
||||
Assert.Null(sheet.Cells[0, 0].Value);
|
||||
|
||||
view.Commands.Undo();
|
||||
|
||||
Assert.Equal("A", sheet.Cells[0, 0].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Commands_IndependentPerView()
|
||||
{
|
||||
var sheet1 = new Sheet(5, 5);
|
||||
var sheet2 = new Sheet(5, 5);
|
||||
var view1 = new SheetView(sheet1);
|
||||
var view2 = new SheetView(sheet2);
|
||||
|
||||
sheet1.Cells[0, 0].Value = "S1";
|
||||
sheet2.Cells[0, 0].Value = "S2";
|
||||
|
||||
var cmd1 = new ClearContentsCommand(sheet1, new RangeRef(new CellRef(0, 0), new CellRef(0, 0)));
|
||||
view1.Commands.Execute(cmd1);
|
||||
|
||||
Assert.Null(sheet1.Cells[0, 0].Value);
|
||||
Assert.Equal("S2", sheet2.Cells[0, 0].Value);
|
||||
|
||||
// Undo on view1 doesn't affect view2
|
||||
view1.Commands.Undo();
|
||||
Assert.Equal("S1", sheet1.Cells[0, 0].Value);
|
||||
Assert.Equal("S2", sheet2.Cells[0, 0].Value);
|
||||
|
||||
Assert.False(view2.Commands.CanUndo);
|
||||
}
|
||||
}
|
||||
|
||||
public class WorkbookViewTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_SetsWorkbook()
|
||||
{
|
||||
var wb = new Workbook();
|
||||
var view = new WorkbookView(wb);
|
||||
|
||||
Assert.Same(wb, view.Workbook);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ThrowsOnNull()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new WorkbookView(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetView_CreatesViewForSheet()
|
||||
{
|
||||
var wb = new Workbook();
|
||||
var sheet = wb.AddSheet("Sheet1", 10, 10);
|
||||
var view = new WorkbookView(wb);
|
||||
|
||||
var sheetView = view.GetView(sheet);
|
||||
|
||||
Assert.NotNull(sheetView);
|
||||
Assert.Same(sheet, sheetView.Sheet);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetView_ReturnsSameViewForSameSheet()
|
||||
{
|
||||
var wb = new Workbook();
|
||||
var sheet = wb.AddSheet("Sheet1", 10, 10);
|
||||
var view = new WorkbookView(wb);
|
||||
|
||||
var v1 = view.GetView(sheet);
|
||||
var v2 = view.GetView(sheet);
|
||||
|
||||
Assert.Same(v1, v2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetView_DifferentViewsForDifferentSheets()
|
||||
{
|
||||
var wb = new Workbook();
|
||||
var s1 = wb.AddSheet("Sheet1", 10, 10);
|
||||
var s2 = wb.AddSheet("Sheet2", 10, 10);
|
||||
var view = new WorkbookView(wb);
|
||||
|
||||
var v1 = view.GetView(s1);
|
||||
var v2 = view.GetView(s2);
|
||||
|
||||
Assert.NotSame(v1, v2);
|
||||
Assert.Same(s1, v1.Sheet);
|
||||
Assert.Same(s2, v2.Sheet);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetView_PerSheetUndoHistory()
|
||||
{
|
||||
var wb = new Workbook();
|
||||
var s1 = wb.AddSheet("Sheet1", 5, 5);
|
||||
var s2 = wb.AddSheet("Sheet2", 5, 5);
|
||||
var wbView = new WorkbookView(wb);
|
||||
|
||||
s1.Cells[0, 0].Value = "A";
|
||||
s2.Cells[0, 0].Value = "B";
|
||||
|
||||
var v1 = wbView.GetView(s1);
|
||||
var v2 = wbView.GetView(s2);
|
||||
|
||||
// Execute command on sheet1's view
|
||||
v1.Commands.Execute(new ClearContentsCommand(s1, new RangeRef(new CellRef(0, 0), new CellRef(0, 0))));
|
||||
|
||||
Assert.True(v1.Commands.CanUndo);
|
||||
Assert.False(v2.Commands.CanUndo);
|
||||
|
||||
// Sheet2 is unaffected
|
||||
Assert.Equal("B", s2.Cells[0, 0].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Remove_DisposesView()
|
||||
{
|
||||
var wb = new Workbook();
|
||||
var sheet = wb.AddSheet("Sheet1", 10, 10);
|
||||
var view = new WorkbookView(wb);
|
||||
|
||||
var v1 = view.GetView(sheet);
|
||||
v1.Commands.Execute(new ClearContentsCommand(sheet, new RangeRef(new CellRef(0, 0), new CellRef(0, 0))));
|
||||
|
||||
Assert.True(view.Remove(sheet));
|
||||
|
||||
// New view has empty undo stack
|
||||
var v2 = view.GetView(sheet);
|
||||
Assert.NotSame(v1, v2);
|
||||
Assert.False(v2.Commands.CanUndo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Remove_ReturnsFalseForUnknownSheet()
|
||||
{
|
||||
var wb = new Workbook();
|
||||
var view = new WorkbookView(wb);
|
||||
var sheet = new Sheet(5, 5);
|
||||
|
||||
Assert.False(view.Remove(sheet));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetView_ThrowsOnNull()
|
||||
{
|
||||
var wb = new Workbook();
|
||||
var view = new WorkbookView(wb);
|
||||
|
||||
Assert.Throws<ArgumentNullException>(() => view.GetView(null!));
|
||||
}
|
||||
}
|
||||
95
Radzen.Blazor/Spreadsheet/SheetView.cs
Normal file
95
Radzen.Blazor/Spreadsheet/SheetView.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
namespace Radzen.Blazor.Spreadsheet;
|
||||
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// Holds per-sheet UI state (undo/redo history, rendering layout) that is not part of the document model.
|
||||
/// </summary>
|
||||
public class SheetView
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the document sheet this view wraps.
|
||||
/// </summary>
|
||||
public Sheet Sheet { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the per-sheet undo/redo stack.
|
||||
/// </summary>
|
||||
public UndoRedoStack Commands { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the header offset for rows (height of column headers in pixels).
|
||||
/// </summary>
|
||||
public double RowHeaderOffset { get; set; } = 24;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the header offset for columns (width of row headers in pixels).
|
||||
/// </summary>
|
||||
public double ColumnHeaderOffset { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SheetView"/> class.
|
||||
/// </summary>
|
||||
public SheetView(Sheet sheet)
|
||||
{
|
||||
Sheet = sheet ?? throw new System.ArgumentNullException(nameof(sheet));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the visible row index range for the given scroll position and viewport height.
|
||||
/// </summary>
|
||||
public IndexRange GetRowRange(double start, double end, bool includeFrozen = false)
|
||||
{
|
||||
return Sheet.Rows.GetIndexRange(start, end, includeFrozen);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the visible column index range for the given scroll position and viewport width.
|
||||
/// </summary>
|
||||
public IndexRange GetColumnRange(double start, double end, bool includeFrozen = false)
|
||||
{
|
||||
return Sheet.Columns.GetIndexRange(start, end, includeFrozen);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pixel range for the specified row indices.
|
||||
/// </summary>
|
||||
public PixelRange GetRowPixelRange(int startIndex, int endIndex)
|
||||
{
|
||||
return Sheet.Rows.GetPixelRange(startIndex, endIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pixel range for a single row index.
|
||||
/// </summary>
|
||||
public PixelRange GetRowPixelRange(int index)
|
||||
{
|
||||
return Sheet.Rows.GetPixelRange(index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pixel range for the specified column indices.
|
||||
/// </summary>
|
||||
public PixelRange GetColumnPixelRange(int startIndex, int endIndex)
|
||||
{
|
||||
return Sheet.Columns.GetPixelRange(startIndex, endIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pixel range for a single column index.
|
||||
/// </summary>
|
||||
public PixelRange GetColumnPixelRange(int index)
|
||||
{
|
||||
return Sheet.Columns.GetPixelRange(index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total scrollable height including all visible rows and the header offset.
|
||||
/// </summary>
|
||||
public double TotalHeight => Sheet.Rows.Total;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total scrollable width including all visible columns and the header offset.
|
||||
/// </summary>
|
||||
public double TotalWidth => Sheet.Columns.Total;
|
||||
}
|
||||
53
Radzen.Blazor/Spreadsheet/WorkbookView.cs
Normal file
53
Radzen.Blazor/Spreadsheet/WorkbookView.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet;
|
||||
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// Manages per-sheet UI state (SheetView instances) for a workbook.
|
||||
/// Each sheet gets its own view with independent undo/redo history and rendering state.
|
||||
/// </summary>
|
||||
public class WorkbookView
|
||||
{
|
||||
private readonly Dictionary<Sheet, SheetView> views = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the workbook this view wraps.
|
||||
/// </summary>
|
||||
public Workbook Workbook { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WorkbookView"/> class.
|
||||
/// </summary>
|
||||
public WorkbookView(Workbook workbook)
|
||||
{
|
||||
Workbook = workbook ?? throw new ArgumentNullException(nameof(workbook));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or creates a SheetView for the specified sheet.
|
||||
/// </summary>
|
||||
public SheetView GetView(Sheet sheet)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(sheet);
|
||||
|
||||
if (!views.TryGetValue(sheet, out var view))
|
||||
{
|
||||
view = new SheetView(sheet);
|
||||
views[sheet] = view;
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the view for the specified sheet, freeing its undo history and rendering state.
|
||||
/// </summary>
|
||||
public bool Remove(Sheet sheet)
|
||||
{
|
||||
return views.Remove(sheet);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user