Extract HeaderBase for ColumnHeader and RowHeader

Both components had ~100 lines of identical code for selection state
tracking, event subscription, and disposal. Extract shared logic
into HeaderBase with four one-liner axis overrides per subclass.
This commit is contained in:
Atanas Korchev
2026-03-22 16:59:19 +02:00
committed by Vladimir Enchev
parent b1cbf3ef4a
commit c349b62d46
5 changed files with 166 additions and 175 deletions

View File

@@ -1,6 +1,6 @@
@using Radzen.Documents.Spreadsheet
@using Microsoft.AspNetCore.Components.Web
@inherits CellBase
@inherits HeaderBase
<div class=@Class style=@Style data-column=@Column>
@ColumnRef.ToString(Column)
</div>

View File

@@ -1,7 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Components;
using Radzen.Blazor.Rendering;
using System;
using System.Threading.Tasks;
using Radzen.Documents.Spreadsheet;
namespace Radzen.Blazor.Spreadsheet;
@@ -10,7 +9,7 @@ namespace Radzen.Blazor.Spreadsheet;
/// <summary>
/// Renders a column header in a spreadsheet.
/// </summary>
public partial class ColumnHeader : CellBase, IDisposable
public partial class ColumnHeader : HeaderBase
{
/// <summary>
/// Gets or sets the column index of the column header.
@@ -18,95 +17,26 @@ public partial class ColumnHeader : CellBase, IDisposable
[Parameter]
public int Column { get; set; }
/// <summary>
/// Gets or sets the sheet that contains the column header.
/// </summary>
[Parameter]
public Worksheet? Worksheet { get; set; }
/// <inheritdoc/>
protected override int Index => Column;
private bool active;
private bool selected;
/// <inheritdoc/>
protected override string IndexParameterName => nameof(Column);
/// <inheritdoc/>
[SuppressMessage("Design", "CA1062", Justification = "Base class guarantees non-null.")]
protected override bool CheckIsActive(Selection selection) => selection.IsActive(new ColumnRef(Column));
/// <inheritdoc/>
[SuppressMessage("Design", "CA1062", Justification = "Base class guarantees non-null.")]
protected override bool CheckIsSelected(Selection selection) => selection.IsSelected(new ColumnRef(Column));
private string Class => ClassList.Create("rz-spreadsheet-column-header")
.Add("rz-spreadsheet-frozen-row", FrozenState.HasFlag(FrozenState.Row))
.Add("rz-spreadsheet-frozen-column", FrozenState.HasFlag(FrozenState.Column))
.Add("rz-spreadsheet-header-active", active)
.Add("rz-spreadsheet-header-selected", selected)
.Add("rz-spreadsheet-header-active", Active)
.Add("rz-spreadsheet-header-selected", Selected)
.ToString();
private string ResizeHandleStyle => $"left: {Rect.Right.ToPx()}; top: {Rect.Top.ToPx()}; height: {Rect.Height.ToPx()};";
/// <inheritdoc/>
protected override void OnInitialized()
{
UpdateState();
}
/// <inheritdoc/>
public override async Task SetParametersAsync(ParameterView parameters)
{
if (Worksheet != null)
{
Worksheet.Selection.Changed -= OnSelectionChanged;
}
var didColumnChange = parameters.TryGetValue<int>(nameof(Column), out var column) && Column != column;
await base.SetParametersAsync(parameters);
if (Worksheet != null)
{
Worksheet.Selection.Changed += OnSelectionChanged;
}
if (didColumnChange)
{
UpdateState();
}
}
private void OnSelectionChanged()
{
var dirty = UpdateState();
if (dirty)
{
StateHasChanged();
}
}
private bool UpdateState()
{
var dirty = false;
if (Worksheet is not null)
{
var address = new ColumnRef(Column);
if (Worksheet.Selection.IsActive(address) != active)
{
active = !active;
dirty = true;
}
if (Worksheet.Selection.IsSelected(address) != selected)
{
selected = !selected;
dirty = true;
}
}
return dirty;
}
void IDisposable.Dispose()
{
if (Worksheet != null)
{
Worksheet.Selection.Changed -= OnSelectionChanged;
}
}
}
}

View File

@@ -0,0 +1,130 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Radzen.Blazor.Rendering;
using Radzen.Documents.Spreadsheet;
namespace Radzen.Blazor.Spreadsheet;
#nullable enable
/// <summary>
/// Base class for row and column header components. Manages selection state tracking,
/// event subscription lifecycle, and CSS class generation.
/// Subclasses provide the axis-specific index, address creation, and CSS class name.
/// </summary>
public abstract class HeaderBase : CellBase, IDisposable
{
/// <summary>
/// Gets or sets the worksheet that contains this header.
/// </summary>
[Parameter]
public Worksheet? Worksheet { get; set; }
private bool active;
/// <summary>
/// Gets whether the header is in the active selection range.
/// </summary>
protected bool Active => active;
private bool selected;
/// <summary>
/// Gets whether the header's entire row/column is selected.
/// </summary>
protected bool Selected => selected;
/// <summary>
/// Gets the axis index (row or column) for this header.
/// </summary>
protected abstract int Index { get; }
/// <summary>
/// Gets the parameter name to watch for changes (e.g. "Row" or "Column").
/// </summary>
protected abstract string IndexParameterName { get; }
/// <summary>
/// Returns whether the selection is active for this header's axis position.
/// Called only after validating that <paramref name="selection"/> is non-null.
/// </summary>
protected abstract bool CheckIsActive(Selection selection);
/// <summary>
/// Returns whether the selection covers this header's entire row/column.
/// Called only after validating that <paramref name="selection"/> is non-null.
/// </summary>
protected abstract bool CheckIsSelected(Selection selection);
/// <inheritdoc/>
protected override void OnInitialized()
{
UpdateState();
}
/// <inheritdoc/>
public override async Task SetParametersAsync(ParameterView parameters)
{
if (Worksheet != null)
{
Worksheet.Selection.Changed -= OnSelectionChanged;
}
var didIndexChange = parameters.TryGetValue<int>(IndexParameterName, out var index) && Index != index;
await base.SetParametersAsync(parameters);
if (Worksheet != null)
{
Worksheet.Selection.Changed += OnSelectionChanged;
}
if (didIndexChange)
{
UpdateState();
}
}
private void OnSelectionChanged()
{
if (UpdateState())
{
StateHasChanged();
}
}
private bool UpdateState()
{
var dirty = false;
if (Worksheet is not null)
{
var selection = Worksheet.Selection;
if (CheckIsActive(selection) != active)
{
active = !active;
dirty = true;
}
if (CheckIsSelected(selection) != selected)
{
selected = !selected;
dirty = true;
}
}
return dirty;
}
/// <inheritdoc/>
void IDisposable.Dispose()
{
if (Worksheet != null)
{
Worksheet.Selection.Changed -= OnSelectionChanged;
}
}
}

View File

@@ -1,6 +1,6 @@
@using Radzen.Documents.Spreadsheet
@using Microsoft.AspNetCore.Components.Web
@inherits CellBase
@inherits HeaderBase
<div class=@Class style=@Style data-row=@Row>
@(Row + 1)
</div>

View File

@@ -1,8 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Components;
using Radzen.Blazor.Rendering;
using System;
using System.Threading.Tasks;
using Radzen.Documents.Spreadsheet;
namespace Radzen.Blazor.Spreadsheet;
@@ -12,7 +10,7 @@ namespace Radzen.Blazor.Spreadsheet;
/// <summary>
/// Renders a row header in a spreadsheet.
/// </summary>
public partial class RowHeader : CellBase, IDisposable
public partial class RowHeader : HeaderBase
{
/// <summary>
/// Gets or sets the row index for the row header.
@@ -20,93 +18,26 @@ public partial class RowHeader : CellBase, IDisposable
[Parameter]
public int Row { get; set; }
/// <summary>
/// Gets or sets the sheet that contains the row header.
/// </summary>
[Parameter]
public Worksheet? Worksheet { get; set; }
/// <inheritdoc/>
protected override int Index => Row;
private bool active;
private bool selected;
/// <inheritdoc/>
protected override string IndexParameterName => nameof(Row);
/// <inheritdoc/>
[SuppressMessage("Design", "CA1062", Justification = "Base class guarantees non-null.")]
protected override bool CheckIsActive(Selection selection) => selection.IsActive(new RowRef(Row));
/// <inheritdoc/>
[SuppressMessage("Design", "CA1062", Justification = "Base class guarantees non-null.")]
protected override bool CheckIsSelected(Selection selection) => selection.IsSelected(new RowRef(Row));
private string Class => ClassList.Create("rz-spreadsheet-row-header")
.Add("rz-spreadsheet-frozen-row", FrozenState.HasFlag(FrozenState.Row))
.Add("rz-spreadsheet-frozen-column", FrozenState.HasFlag(FrozenState.Column))
.Add("rz-spreadsheet-header-active", active)
.Add("rz-spreadsheet-header-selected", selected)
.Add("rz-spreadsheet-header-active", Active)
.Add("rz-spreadsheet-header-selected", Selected)
.ToString();
private string ResizeHandleStyle => $"top: {Rect.Bottom.ToPx()}; left: {Rect.Left.ToPx()}; width: {Rect.Width.ToPx()};";
/// <inheritdoc/>
protected override void OnInitialized()
{
UpdateState();
}
/// <inheritdoc/>
public override async Task SetParametersAsync(ParameterView parameters)
{
if (Worksheet != null)
{
Worksheet.Selection.Changed -= OnSelectionChanged;
}
var didRowChange = parameters.TryGetValue<int>(nameof(Row), out var row) && Row != row;
await base.SetParametersAsync(parameters);
if (Worksheet != null)
{
Worksheet.Selection.Changed += OnSelectionChanged;
}
if (didRowChange)
{
UpdateState();
}
}
private void OnSelectionChanged()
{
var dirty = UpdateState();
if (dirty)
{
StateHasChanged();
}
}
private bool UpdateState()
{
var dirty = false;
if (Worksheet is not null)
{
var address = new RowRef(Row);
if (Worksheet.Selection.IsActive(address) != active)
{
active = !active;
dirty = true;
}
if (Worksheet.Selection.IsSelected(address) != selected)
{
selected = !selected;
dirty = true;
}
}
return dirty;
}
void IDisposable.Dispose()
{
if (Worksheet != null)
{
Worksheet.Selection.Changed -= OnSelectionChanged;
}
}
}
}