Files
radzen-blazor/Radzen.Blazor/Spreadsheet/CellEditor.razor.cs
Atanas Korchev 609622692e Add culture-aware editing, display, and formula entry to RadzenSpreadsheet
The spreadsheet engine parsed typed input with the invariant culture while
displaying values with the current culture, breaking the edit round-trip on
non-en-US hosts and making comma-decimal entry (10,50) impossible.

Workbook gains a Culture property (defaults to CurrentCulture) which the
component stamps from its inherited Culture parameter. It drives:

- Cell input parsing (CellData type inference), including day-month date
  handling for cultures where the group and date separators collide (de-DE)
- Edit text and display rendering (GetValue, GetValueAsString, GetDisplayText)
- Number format rendering - format codes stay canonical invariant tokens while
  separators, month names, and AM/PM designators follow the culture
- Formula entry and display via the new FormulaLocalizer (Excel FormulaLocal
  semantics: ';' argument separators and ',' decimals in comma-decimal
  cultures, lenient comma acceptance where unambiguous, canonical invariant
  storage)
- Dialog input (data validation, conditional format, filter) via shared
  conversion helpers on SpreadsheetDialogBase

XLSX and CSV files read and write canonical invariant values regardless of
the workbook culture, including autofit column widths. Malformed formulas now
surface as error trees instead of an unhandled lexer exception. Number format
parsing is cached (hard-bounded) since CellView reparsed per render.

Includes localization demos for the Spreadsheet and Document Processing
sections and culture test suites.

Note: headless code on non-en-US hosts now parses string values with the
host culture; set Workbook.Culture explicitly (or to InvariantCulture) for
host-independent processing.
2026-07-08 14:54:55 +03:00

199 lines
5.4 KiB
C#

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Radzen.Blazor.Rendering;
#nullable enable
using Radzen.Documents.Spreadsheet;
namespace Radzen.Blazor.Spreadsheet;
/// <summary>
/// Renders an inline cell editor for a spreadsheet.
/// </summary>
public partial class CellEditor : ComponentBase, IDisposable
{
/// <summary>
/// Gets or sets the sheet.
/// </summary>
[Parameter]
public Worksheet Worksheet { get; set; } = default!;
/// <summary>
/// Gets or sets the editor.
/// </summary>
[Parameter]
public Editor Editor { get; set; } = default!;
/// <summary>
/// Gets or sets the virtual grid context.
/// </summary>
[Parameter]
public IVirtualGridContext Context { get; set; } = default!;
/// <summary>
/// Gets or sets the spreadsheet instance that contains this cell editor.
/// </summary>
[CascadingParameter]
public ISpreadsheet Spreadsheet { get; set; } = default!;
/// <summary>
/// Gets or sets the registered custom cell types.
/// </summary>
[CascadingParameter]
public Dictionary<string, SpreadsheetCellType>? CellTypes { get; set; }
private string? cellStyle;
private string? editorStyle;
private string? className;
private Type? customEditorType;
private readonly EventBinding<Editor> editorBinding;
/// <summary>
/// Initializes a new instance of the <see cref="CellEditor"/> class.
/// </summary>
public CellEditor()
{
editorBinding = new EventBinding<Editor>(
e =>
{
e.Changed += OnEditModeChanged;
e.ValueChanged += OnEditorValueChanged;
},
e =>
{
e.Changed -= OnEditModeChanged;
e.ValueChanged -= OnEditorValueChanged;
});
}
/// <inheritdoc />
protected override void OnParametersSet()
{
editorBinding.Bind(Worksheet is not null ? Editor : null);
if (Worksheet is not null)
{
Render();
}
}
private async Task OnBlurAsync()
{
if (Editor.Mode == EditMode.Cell)
{
await Spreadsheet.AcceptAsync();
}
}
private void OnFocus()
{
if (Worksheet.Selection.Cell != CellRef.Invalid)
{
var cell = Worksheet.Cells[Worksheet.Selection.Cell];
Editor.StartEdit(cell.Address, Editor.Mode != EditMode.None ? Editor.Value : cell.GetValue(), EditMode.Cell);
}
}
private void OnEditorValueChanged()
{
if (Editor.Mode == EditMode.Formula)
{
StateHasChanged();
}
}
private void Render()
{
var address = Worksheet.Selection.Cell;
if (address == CellRef.Invalid)
{
cellStyle = null;
className = "rz-spreadsheet-cell-editor";
return;
}
var cell = Worksheet.Cells[address];
if (address != CellRef.Invalid)
{
var rect = Context.GetRectangle(address.Row, address.Column);
var sb = StringBuilderCache.Acquire();
rect.AppendStyle(sb);
cell = Worksheet.Cells[address];
cellStyle = StringBuilderCache.GetStringAndRelease(sb);
var fontSb = StringBuilderCache.Acquire();
cell.GetEffectiveFormat()?.AppendFontStyle(fontSb);
var fontStyle = StringBuilderCache.GetStringAndRelease(fontSb);
editorStyle = fontStyle.Length > 0 ? fontStyle : null;
}
else
{
cellStyle = null;
}
className = ClassList.Create("rz-spreadsheet-cell-editor")
.Add("rz-spreadsheet-frozen-column", Worksheet.Selection.Cell != CellRef.Invalid && Worksheet.Selection.Cell.Column < Worksheet.Columns.Frozen)
.Add("rz-spreadsheet-frozen-row", Worksheet.Selection.Cell != CellRef.Invalid && Worksheet.Selection.Cell.Row < Worksheet.Rows.Frozen)
.Add($"rz-spreadsheet-cell-editor-{cell.ValueType.ToString().ToLowerInvariant()}", cell is not null)
.ToString();
}
private Type? ResolveEditorType()
{
if (CellTypes is not { Count: > 0 } || Worksheet is null)
{
return null;
}
var address = Worksheet.Selection.Cell;
if (address == CellRef.Invalid)
{
return null;
}
var typeName = Worksheet.Cells.GetCustomType(address.Row, address.Column);
if (typeName is not null && CellTypes.TryGetValue(typeName, out var cellType))
{
return cellType.EditorType;
}
return null;
}
private Dictionary<string, object> GetEditorParameters()
{
var address = Worksheet.Selection.Cell;
var cell = address != CellRef.Invalid ? Worksheet.Cells[address] : null;
var formattedValue = cell?.GetValueAsString();
var context = new SpreadsheetCellEditContext(
formattedValue, cell!, Worksheet, Editor, Spreadsheet
);
return new Dictionary<string, object>
{
{ "Context", context }
};
}
private void OnEditModeChanged()
{
Render();
customEditorType = ResolveEditorType();
StateHasChanged();
}
void IDisposable.Dispose()
{
editorBinding.Dispose();
}
}