mirror of
https://github.com/radzenhq/radzen-blazor.git
synced 2026-07-08 18:16:08 +00:00
Fix RadzenSpreadsheet edit refresh, table sort, filter and tool issues
- Edited value cells now repaint: fire the changed cell's notification even inside a command batch (only dependent recalc is deferred to EndUpdate). - Table sort excludes the header (and totals) row by sorting DataBodyRange; the cell-menu and context-menu sorts are now table/auto-filter aware. - Re-apply active filters after a sort (and on sort undo) so filtered-out rows stay hidden by value. - Enable Merge, Borders, Conditional and Data Validation tools when a range is selected by subscribing to Selection.Changed. - Keep the worksheet prefix on cross-sheet references in the formula-bar syntax highlighter (use the raw token text). Adds regression tests for all of the above.
This commit is contained in:
52
Radzen.Blazor.Tests/Spreadsheet/FormulaDisplayTests.cs
Normal file
52
Radzen.Blazor.Tests/Spreadsheet/FormulaDisplayTests.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using Bunit;
|
||||
using Xunit;
|
||||
|
||||
using Radzen.Documents.Spreadsheet;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
public class FormulaDisplayTests
|
||||
{
|
||||
[Fact]
|
||||
public void CrossSheetFormula_ModelRetainsSheetPrefix()
|
||||
{
|
||||
var wb = new Workbook();
|
||||
var q1 = wb.AddSheet("Q1 Sales", 20, 10);
|
||||
var summary = wb.AddSheet("Summary", 20, 10);
|
||||
|
||||
q1.Cells["B2"].Value = 10d;
|
||||
q1.Cells["C2"].Value = 20d;
|
||||
q1.Cells["D2"].Value = 30d;
|
||||
|
||||
summary.Cells["B2"].Formula = "=SUM('Q1 Sales'!B2:D2)";
|
||||
|
||||
Assert.Equal(60d, summary.Cells["B2"].Value);
|
||||
Assert.Equal("=SUM('Q1 Sales'!B2:D2)", summary.Cells["B2"].Formula);
|
||||
Assert.Equal("=SUM('Q1 Sales'!B2:D2)", summary.Cells["B2"].GetValue());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Highlight_CrossSheetReference_KeepsSheetPrefix()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
var cut = ctx.RenderComponent<SheetEditorHighlight>(
|
||||
parameters => parameters.Add(p => p.Value, "=SUM('Q1 Sales'!B2:D2)"));
|
||||
|
||||
// The syntax-highlight overlay must keep the worksheet prefix (it previously rendered
|
||||
// the bare cell address, dropping 'Q1 Sales'!).
|
||||
Assert.Contains("'Q1 Sales'!B2", cut.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Highlight_BareCellReference_StillRenders()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
var cut = ctx.RenderComponent<SheetEditorHighlight>(
|
||||
parameters => parameters.Add(p => p.Value, "=B2+C2"));
|
||||
|
||||
Assert.Contains("B2", cut.Markup);
|
||||
Assert.Contains("C2", cut.Markup);
|
||||
}
|
||||
}
|
||||
144
Radzen.Blazor.Tests/Spreadsheet/SpreadsheetRegressionTests.cs
Normal file
144
Radzen.Blazor.Tests/Spreadsheet/SpreadsheetRegressionTests.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
using Xunit;
|
||||
|
||||
using Radzen.Documents.Spreadsheet;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
// Regressions found during end-user testing of RadzenSpreadsheet.
|
||||
public class SpreadsheetRegressionTests
|
||||
{
|
||||
// #1 - A batched value edit must still notify the edited cell (regression from wrapping
|
||||
// every command in Worksheet.Batch). Dependents were handled by EndUpdate; the changed
|
||||
// value cell was not.
|
||||
[Fact]
|
||||
public void BatchedValueEdit_NotifiesEditedCell()
|
||||
{
|
||||
var sheet = new Worksheet(10, 10);
|
||||
var changed = 0;
|
||||
sheet.Cells[2, 2].Changed += _ => changed++;
|
||||
|
||||
sheet.Batch(() => sheet.Cells[2, 2].Value = 5d);
|
||||
|
||||
Assert.Equal(5d, sheet.Cells[2, 2].Value);
|
||||
Assert.Equal(1, changed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClearContentsViaStack_NotifiesClearedCell()
|
||||
{
|
||||
var sheet = new Worksheet(10, 10);
|
||||
sheet.Cells[0, 0].Value = 7d;
|
||||
var stack = new UndoRedoStack(sheet);
|
||||
|
||||
var changed = 0;
|
||||
sheet.Cells[0, 0].Changed += _ => changed++;
|
||||
|
||||
stack.Execute(new ClearContentsCommand(sheet, new RangeRef(new CellRef(0, 0), new CellRef(0, 0))));
|
||||
|
||||
Assert.Null(sheet.Cells[0, 0].Value);
|
||||
Assert.True(changed >= 1, "cleared cell should raise Changed");
|
||||
}
|
||||
|
||||
// #2 - Sorting a table must never move data into the header (or totals) row.
|
||||
[Fact]
|
||||
public void TableSort_KeepsHeaderRow()
|
||||
{
|
||||
var wb = new Workbook();
|
||||
var ws = wb.AddSheet("S", 10, 2);
|
||||
ws.Cells[0, 0].SetValue("Name");
|
||||
ws.Cells[0, 1].SetValue("Dept");
|
||||
ws.Cells[1, 0].SetValue("Alice");
|
||||
ws.Cells[2, 0].SetValue("Carol");
|
||||
ws.Cells[3, 0].SetValue("Tina");
|
||||
|
||||
var table = ws.AddTable("T", RangeRef.Parse("A1:B4"));
|
||||
table.Sort(SortOrder.Descending, 0);
|
||||
|
||||
Assert.Equal("Name", ws.Cells[0, 0].Value); // header intact
|
||||
Assert.Equal("Tina", ws.Cells[1, 0].Value); // first data row (descending)
|
||||
Assert.Equal("Carol", ws.Cells[2, 0].Value);
|
||||
Assert.Equal("Alice", ws.Cells[3, 0].Value);
|
||||
}
|
||||
|
||||
// #6 - A sort must re-apply active filters by value, so filtered-out rows stay hidden.
|
||||
private static Worksheet BuildFilteredSheet()
|
||||
{
|
||||
var wb = new Workbook();
|
||||
var ws = wb.AddSheet("S", 20, 2);
|
||||
ws.Cells[0, 0].SetValue("Name");
|
||||
ws.Cells[0, 1].SetValue("Dept");
|
||||
var data = new[]
|
||||
{
|
||||
("Alice", "Eng"), ("Bob", "Mkt"), ("Carol", "Eng"),
|
||||
("Dave", "Sales"), ("Tina", "Mkt"), ("Sam", "Eng"),
|
||||
};
|
||||
for (var i = 0; i < data.Length; i++)
|
||||
{
|
||||
ws.Cells[i + 1, 0].SetValue(data[i].Item1);
|
||||
ws.Cells[i + 1, 1].SetValue(data[i].Item2);
|
||||
}
|
||||
|
||||
ws.AutoFilter.Range = new RangeRef(new CellRef(0, 0), new CellRef(6, 1));
|
||||
ws.AutoFilter.ApplyValueFilter(1, ["Eng"]);
|
||||
return ws;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SortPreservesActiveFilter()
|
||||
{
|
||||
var ws = BuildFilteredSheet();
|
||||
|
||||
ws.Sort(new RangeRef(new CellRef(1, 0), new CellRef(6, 1)),
|
||||
new SortKey { ColumnIndex = 0, Order = SortOrder.Descending });
|
||||
|
||||
var visible = 0;
|
||||
for (var r = 1; r <= 6; r++)
|
||||
{
|
||||
if (!ws.Rows.IsHidden(r))
|
||||
{
|
||||
visible++;
|
||||
Assert.Equal("Eng", ws.Cells[r, 1].Value);
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Equal(3, visible); // Alice, Carol, Sam
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UndoSortRestoresFilter()
|
||||
{
|
||||
var ws = BuildFilteredSheet();
|
||||
var stack = new UndoRedoStack(ws);
|
||||
|
||||
stack.Execute(new SortCommand(ws, new RangeRef(new CellRef(1, 0), new CellRef(6, 1)),
|
||||
SortOrder.Descending, 0));
|
||||
stack.Undo();
|
||||
|
||||
// Original order restored; filter re-applied by value -> only the Eng rows visible.
|
||||
Assert.Equal("Alice", ws.Cells[1, 0].Value);
|
||||
Assert.False(ws.Rows.IsHidden(1)); // Alice / Eng
|
||||
Assert.True(ws.Rows.IsHidden(2)); // Bob / Mkt
|
||||
Assert.False(ws.Rows.IsHidden(3)); // Carol / Eng
|
||||
Assert.True(ws.Rows.IsHidden(4)); // Dave / Sales
|
||||
Assert.True(ws.Rows.IsHidden(5)); // Tina / Mkt
|
||||
Assert.False(ws.Rows.IsHidden(6)); // Sam / Eng
|
||||
}
|
||||
|
||||
// #3 - the now-enabled Merge button's action must merge the selected range (the command
|
||||
// the toolbar dispatches: MergeCellsCommand over Selection.Range).
|
||||
[Fact]
|
||||
public void MergeCommand_MergesSelectionRange()
|
||||
{
|
||||
var sheet = new Worksheet(10, 10);
|
||||
var stack = new UndoRedoStack(sheet);
|
||||
var range = new RangeRef(new CellRef(4, 5), new CellRef(5, 6)); // F5:G6
|
||||
sheet.Selection.Select(range);
|
||||
|
||||
stack.Execute(new MergeCellsCommand(sheet, sheet.Selection.Range, center: true));
|
||||
|
||||
Assert.Equal(range, sheet.MergedCells.GetMergedRange(new CellRef(4, 5)));
|
||||
|
||||
stack.Undo();
|
||||
Assert.Equal(RangeRef.Invalid, sheet.MergedCells.GetMergedRange(new CellRef(4, 5)));
|
||||
}
|
||||
}
|
||||
@@ -339,7 +339,8 @@ public class Table
|
||||
/// </summary>
|
||||
public void Sort(SortOrder order, int column)
|
||||
{
|
||||
Worksheet.Sort(range, new SortKey { ColumnIndex = column, Order = order });
|
||||
// Sort only the data body - never the header or totals row.
|
||||
Worksheet.Sort(DataBodyRange, new SortKey { ColumnIndex = column, Order = order });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -348,7 +349,8 @@ public class Table
|
||||
/// </summary>
|
||||
public void Sort(params SortKey[] keys)
|
||||
{
|
||||
Worksheet.Sort(range, keys);
|
||||
// Sort only the data body - never the header or totals row.
|
||||
Worksheet.Sort(DataBodyRange, keys);
|
||||
}
|
||||
|
||||
/// <summary>Resizes the table to a new range. Columns are added or trimmed accordingly.</summary>
|
||||
|
||||
@@ -840,6 +840,19 @@ public partial class Worksheet
|
||||
Rows.EndUpdate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-evaluates all active filters against current cell values, so rows are hidden by value
|
||||
/// at their current positions. Call after a sort (Excel re-applies filters by value). No-op
|
||||
/// when no filters are active, so it never disturbs manually hidden rows.
|
||||
/// </summary>
|
||||
internal void ReapplyFilters()
|
||||
{
|
||||
if (filters.Count > 0)
|
||||
{
|
||||
ApplyFilters();
|
||||
}
|
||||
}
|
||||
|
||||
private void Filter(RangeRef range, FilterCriterion criterion)
|
||||
{
|
||||
if (range == RangeRef.Invalid)
|
||||
|
||||
@@ -98,6 +98,9 @@ public partial class Worksheet
|
||||
// CopyFrom re-triggers formula evaluation, which on a sheet with aggregate
|
||||
// formulas (e.g. SUM over the sorted range) is O(N^2) per sort.
|
||||
Batch(() => WriteSortedRows(range, sortedRows));
|
||||
|
||||
// Excel re-applies active filters by value after a sort, so filtered-out rows stay hidden.
|
||||
ReapplyFilters();
|
||||
}
|
||||
|
||||
private void WriteSortedRows(RangeRef range, List<(int originalIndex, List<Cell> cells)> sortedRows)
|
||||
|
||||
@@ -383,7 +383,14 @@ public partial class Worksheet
|
||||
|
||||
internal void OnCellValueChanged(Cell cell)
|
||||
{
|
||||
if (!IsUpdating && !isEvaluating)
|
||||
if (isEvaluating)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// During a batch, EndUpdate recalculates dependent formulas once. The changed cell
|
||||
// itself is not a formula node, so it would never be notified - fire its event here.
|
||||
if (!IsUpdating)
|
||||
{
|
||||
var dependents = graph.GetTopologicallySortedDependencies(cell);
|
||||
|
||||
@@ -391,9 +398,9 @@ public partial class Worksheet
|
||||
{
|
||||
EvaluateFormula(dependentCell);
|
||||
}
|
||||
|
||||
cell.OnChanged();
|
||||
}
|
||||
|
||||
cell.OnChanged();
|
||||
}
|
||||
|
||||
internal void OnCellFormulaChanged(Cell cell)
|
||||
|
||||
@@ -671,23 +671,7 @@ public partial class RadzenSpreadsheet : RadzenComponent, IAsyncDisposable, ISpr
|
||||
{
|
||||
if (Worksheet != null)
|
||||
{
|
||||
// Check if we're in a data table
|
||||
foreach (var table in Worksheet.Tables)
|
||||
{
|
||||
if (table.Range.Contains(cellMenuRow, cellMenuColumn))
|
||||
{
|
||||
var command = new SortCommand(Worksheet, table.Range, order, cellMenuColumn);
|
||||
await ExecuteAsync(command);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we're in an auto filter
|
||||
if (Worksheet.AutoFilter.Range is not null && Worksheet.AutoFilter.Range.Value.Contains(cellMenuRow, cellMenuColumn))
|
||||
{
|
||||
var command = new SortCommand(Worksheet, Worksheet.AutoFilter.Range.Value, order, cellMenuColumn, skipHeaderRow: true);
|
||||
await ExecuteAsync(command);
|
||||
}
|
||||
await ExecuteAsync(BuildSortCommand(cellMenuRow, cellMenuColumn, order));
|
||||
}
|
||||
|
||||
if (cellMenuPopup != null)
|
||||
@@ -696,6 +680,26 @@ public partial class RadzenSpreadsheet : RadzenComponent, IAsyncDisposable, ISpr
|
||||
}
|
||||
}
|
||||
|
||||
// Builds a sort command scoped to the data the clicked cell belongs to, never including a
|
||||
// table/auto-filter header (or table totals) row in the sorted range.
|
||||
private SortCommand BuildSortCommand(int row, int column, SortOrder order)
|
||||
{
|
||||
foreach (var table in Worksheet!.Tables)
|
||||
{
|
||||
if (table.Range.Contains(row, column))
|
||||
{
|
||||
return new SortCommand(Worksheet, table.DataBodyRange, order, column);
|
||||
}
|
||||
}
|
||||
|
||||
if (Worksheet.AutoFilter.Range is { } afRange && afRange.Contains(row, column))
|
||||
{
|
||||
return new SortCommand(Worksheet, afRange, order, column, skipHeaderRow: true);
|
||||
}
|
||||
|
||||
return new SortCommand(Worksheet, new RangeRef(new CellRef(0, 0), new CellRef(Worksheet.RowCount - 1, Worksheet.ColumnCount - 1)), order, column);
|
||||
}
|
||||
|
||||
private async Task OnCellMenuClearAsync()
|
||||
{
|
||||
if (Worksheet != null)
|
||||
@@ -1324,10 +1328,10 @@ public partial class RadzenSpreadsheet : RadzenComponent, IAsyncDisposable, ISpr
|
||||
await ExecuteAsync(new ClearContentsCommand(Worksheet, Worksheet.Selection.Range));
|
||||
break;
|
||||
case "sort-ascending":
|
||||
await ExecuteAsync(new SortCommand(Worksheet, new RangeRef(new CellRef(0, 0), new CellRef(Worksheet.RowCount - 1, Worksheet.ColumnCount - 1)), SortOrder.Ascending, column));
|
||||
await ExecuteAsync(BuildSortCommand(row, column, SortOrder.Ascending));
|
||||
break;
|
||||
case "sort-descending":
|
||||
await ExecuteAsync(new SortCommand(Worksheet, new RangeRef(new CellRef(0, 0), new CellRef(Worksheet.RowCount - 1, Worksheet.ColumnCount - 1)), SortOrder.Descending, column));
|
||||
await ExecuteAsync(BuildSortCommand(row, column, SortOrder.Descending));
|
||||
break;
|
||||
case "convert-table-to-range":
|
||||
case "delete-table":
|
||||
|
||||
@@ -65,5 +65,8 @@ public class MultiKeySortCommand : RangeSnapshotCommandBase
|
||||
}
|
||||
|
||||
RestoreSnapshot();
|
||||
|
||||
// RestoreSnapshot only restores cell content, not row visibility - re-apply filters by value.
|
||||
sheet.ReapplyFilters();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,9 @@ public partial class SheetEditorHighlight : ComponentBase
|
||||
{
|
||||
var highlightToken = new HighlightToken
|
||||
{
|
||||
Text = token.Type == FormulaTokenType.CellIdentifier ? token.Address.ToString() : token.Value,
|
||||
// Use the raw lexed text so the highlight overlay aligns with the input and
|
||||
// keeps a cross-sheet reference's worksheet prefix (Address.ToString() drops it).
|
||||
Text = token.Value,
|
||||
Class = GetTokenClassName(token.Type),
|
||||
Style = GetTokenStyle(token.Type, refCount)
|
||||
};
|
||||
|
||||
@@ -75,5 +75,8 @@ public class SortCommand : RangeSnapshotCommandBase
|
||||
}
|
||||
|
||||
RestoreSnapshot();
|
||||
|
||||
// RestoreSnapshot only restores cell content, not row visibility - re-apply filters by value.
|
||||
sheet.ReapplyFilters();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
@using Radzen.Blazor
|
||||
@using Radzen.Blazor.Spreadsheet
|
||||
@using Radzen.Documents.Spreadsheet
|
||||
@implements IDisposable
|
||||
|
||||
<RadzenSplitButton Click=@OnClick Icon="border_all" Disabled=@IsDisabled Variant="Variant.Outlined" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small">
|
||||
<ChildContent>
|
||||
@@ -28,6 +29,30 @@
|
||||
=> Worksheet?.Selection.Cell == CellRef.Invalid
|
||||
|| Spreadsheet?.IsFeatureAllowed(SpreadsheetFeature.CellFormatting) == false;
|
||||
|
||||
private readonly EventBinding<Selection> selectionBinding;
|
||||
|
||||
public RadzenSpreadsheetCellBorders()
|
||||
{
|
||||
selectionBinding = new EventBinding<Selection>(
|
||||
s => s.Changed += OnSelectionChanged,
|
||||
s => s.Changed -= OnSelectionChanged);
|
||||
}
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
selectionBinding.Bind(Worksheet?.Selection);
|
||||
}
|
||||
|
||||
private void OnSelectionChanged()
|
||||
{
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
selectionBinding.Dispose();
|
||||
}
|
||||
|
||||
private async Task OnClick(RadzenSplitButtonItem? item)
|
||||
{
|
||||
if (Worksheet is null || Worksheet.Selection.Cell == CellRef.Invalid || Spreadsheet is null)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
@using Radzen.Blazor.Spreadsheet.Tools
|
||||
@using Radzen.Documents.Spreadsheet
|
||||
@inject DialogService DialogService
|
||||
@implements IDisposable
|
||||
|
||||
<RadzenSplitButton Click=@OnClickAsync Icon="format_color_fill" Text=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_Conditional)) ?? "Conditional") Disabled=@IsDisabled Variant="Variant.Outlined" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small">
|
||||
<ChildContent>
|
||||
@@ -28,6 +29,30 @@
|
||||
=> Worksheet?.Selection.Cell == CellRef.Invalid
|
||||
|| Spreadsheet?.IsFeatureAllowed(SpreadsheetFeature.ConditionalFormatting) == false;
|
||||
|
||||
private readonly EventBinding<Selection> selectionBinding;
|
||||
|
||||
public RadzenSpreadsheetConditionalFormat()
|
||||
{
|
||||
selectionBinding = new EventBinding<Selection>(
|
||||
s => s.Changed += OnSelectionChanged,
|
||||
s => s.Changed -= OnSelectionChanged);
|
||||
}
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
selectionBinding.Bind(Worksheet?.Selection);
|
||||
}
|
||||
|
||||
private void OnSelectionChanged()
|
||||
{
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
selectionBinding.Dispose();
|
||||
}
|
||||
|
||||
private async Task OnClickAsync(RadzenSplitButtonItem? item)
|
||||
{
|
||||
if (Worksheet is null || Worksheet.Selection.Cell == CellRef.Invalid)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
@using Radzen.Blazor.Spreadsheet.Tools
|
||||
@using Radzen.Documents.Spreadsheet
|
||||
@inject DialogService DialogService
|
||||
@implements IDisposable
|
||||
|
||||
<RadzenSplitButton Click=@OnClickAsync Icon="rule" Text=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_Validation)) ?? "Validation") Disabled=@IsDisabled Variant="Variant.Outlined" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small">
|
||||
<ChildContent>
|
||||
@@ -25,6 +26,30 @@
|
||||
=> Worksheet?.Selection.Cell == CellRef.Invalid
|
||||
|| Spreadsheet?.IsFeatureAllowed(SpreadsheetFeature.DataValidation) == false;
|
||||
|
||||
private readonly EventBinding<Selection> selectionBinding;
|
||||
|
||||
public RadzenSpreadsheetDataValidation()
|
||||
{
|
||||
selectionBinding = new EventBinding<Selection>(
|
||||
s => s.Changed += OnSelectionChanged,
|
||||
s => s.Changed -= OnSelectionChanged);
|
||||
}
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
selectionBinding.Bind(Worksheet?.Selection);
|
||||
}
|
||||
|
||||
private void OnSelectionChanged()
|
||||
{
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
selectionBinding.Dispose();
|
||||
}
|
||||
|
||||
private async Task OnClickAsync(RadzenSplitButtonItem? item)
|
||||
{
|
||||
if (Worksheet is null || Worksheet.Selection.Cell == CellRef.Invalid || Spreadsheet is null)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
@using Radzen.Blazor
|
||||
@using Radzen.Blazor.Spreadsheet
|
||||
@using Radzen.Documents.Spreadsheet
|
||||
@implements IDisposable
|
||||
|
||||
<RadzenSplitButton Click=@OnClick Icon="merge_type" Disabled=@IsDisabled Variant="Variant.Outlined" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small">
|
||||
<ChildContent>
|
||||
@@ -24,6 +25,30 @@
|
||||
=> Worksheet?.Selection.Cell == CellRef.Invalid
|
||||
|| Spreadsheet?.IsFeatureAllowed(SpreadsheetFeature.Merging) == false;
|
||||
|
||||
private readonly EventBinding<Selection> selectionBinding;
|
||||
|
||||
public RadzenSpreadsheetMergeCells()
|
||||
{
|
||||
selectionBinding = new EventBinding<Selection>(
|
||||
s => s.Changed += OnSelectionChanged,
|
||||
s => s.Changed -= OnSelectionChanged);
|
||||
}
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
selectionBinding.Bind(Worksheet?.Selection);
|
||||
}
|
||||
|
||||
private void OnSelectionChanged()
|
||||
{
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
selectionBinding.Dispose();
|
||||
}
|
||||
|
||||
private async Task OnClick(RadzenSplitButtonItem? item)
|
||||
{
|
||||
if (Worksheet is null || Worksheet.Selection.Cell == CellRef.Invalid || Spreadsheet is null)
|
||||
|
||||
Reference in New Issue
Block a user