From c6baa75c44343c939de676da74ad97b9d6bb64ea Mon Sep 17 00:00:00 2001 From: Marc Chouteau Date: Sat, 29 Aug 2026 15:42:11 +0200 Subject: [PATCH] Support export of template-only columns in SuperDataGrid Enhance SuperDataGridExporter to export columns defined only with a Template by extracting and normalizing rendered text when no Property or ExportValue is set. Update ExportColumnResolver to strip HTML and decode entities for export. Improve error messages and comments to reflect template export support. Add unit tests for template-only column export. Bump version to 2.0.12. --- SUPERDATAGRIDEXPORTER.md | 22 +++++++++- .../Internal/ExportColumnResolver.cs | 43 ++++++++++++++++++- ...erBlazorComponents.DataGridExporter.csproj | 2 +- .../SuperDataGridExporterTests.cs | 29 +++++++++++++ 4 files changed, 91 insertions(+), 5 deletions(-) diff --git a/SUPERDATAGRIDEXPORTER.md b/SUPERDATAGRIDEXPORTER.md index b74ed76..e9f3fa2 100644 --- a/SUPERDATAGRIDEXPORTER.md +++ b/SUPERDATAGRIDEXPORTER.md @@ -101,8 +101,26 @@ Headers are resolved in this order: 3. **Title** 4. **Property** -Values use **ExportValue** first, then **Property** or **For**. Configure both -overrides when a column is entirely template-driven: +Values use **ExportValue** first, then the rendered text of **Template**, and +finally **Property** or **For**. A template-only column can therefore be +exported without declaring a binding; its title is taken from **Title** and +its textual cell content is taken from the template: + +~~~razor + + + +~~~ + +HTML elements are removed from the exported value and the remaining text is +normalized. For a column whose output is produced only by a child component, +configure **ExportValue** explicitly. Configure both overrides when a column +is entirely template-driven and needs a custom header or value: ~~~razor > Resolve(SuperDataGrid ExtractCellText(cellTemplate, item); + } else if (!string.IsNullOrWhiteSpace(column.Property)) { var untypedAccessor = Accessors.GetOrAdd( @@ -52,14 +57,16 @@ public static IReadOnlyList> Resolve(SuperDataGrid(header, column.FormatString, accessor)); } if (result.Count == 0) + { throw new InvalidOperationException("The grid has no visible exportable columns."); + } return result; } @@ -75,7 +82,9 @@ public static IReadOnlyList> Resolve(SuperDataGrid> Resolve(SuperDataGrid property.GetValue(value), @@ -105,23 +116,51 @@ public static IReadOnlyList> Resolve(SuperDataGrid(RenderFragment template, TItem item) + { + var builder = new RenderTreeBuilder(); + template(item)(builder); + return ExtractRenderedText(builder.GetFrames()); + } + private static string? ExtractHeaderText(RenderFragment? template) { if (template is null) + { return null; + } var builder = new RenderTreeBuilder(); template(builder); - var frames = builder.GetFrames(); + return ExtractRenderedText(builder.GetFrames()); + } + + private static string? ExtractRenderedText(ArrayRange frames) + { var parts = new List(); for (var i = 0; i < frames.Count; i++) { var frame = frames.Array[i]; if (frame.FrameType == RenderTreeFrameType.Text) + { parts.Add(frame.TextContent); + } else if (frame.FrameType == RenderTreeFrameType.Markup) + { parts.Add(HtmlTagRegex().Replace(frame.MarkupContent, " ")); + } + else if (frame.FrameType == RenderTreeFrameType.Attribute + && frame.AttributeValue is RenderFragment childContent) + { + var childBuilder = new RenderTreeBuilder(); + childContent(childBuilder); + var childText = ExtractRenderedText(childBuilder.GetFrames()); + if (!string.IsNullOrWhiteSpace(childText)) + { + parts.Add(childText); + } + } } var text = WhitespaceRegex().Replace(WebUtility.HtmlDecode(string.Join(" ", parts)), " ").Trim(); diff --git a/src/SuperBlazorComponents.DataGridExporter/SuperBlazorComponents.DataGridExporter.csproj b/src/SuperBlazorComponents.DataGridExporter/SuperBlazorComponents.DataGridExporter.csproj index 47aaf72..5975cbf 100644 --- a/src/SuperBlazorComponents.DataGridExporter/SuperBlazorComponents.DataGridExporter.csproj +++ b/src/SuperBlazorComponents.DataGridExporter/SuperBlazorComponents.DataGridExporter.csproj @@ -5,7 +5,7 @@ enable SuperBlazorComponents.DataGridExporter SuperBlazorComponents.DataGridExporter - 2.0.10 + 2.0.12 SuperBlazorComponents.DataGridExporter SuperBlazorComponents DataGrid Exporter Appliman diff --git a/tests/SuperBlazorComponents.Tests/SuperDataGridExporterTests.cs b/tests/SuperBlazorComponents.Tests/SuperDataGridExporterTests.cs index 731dff5..de639c9 100644 --- a/tests/SuperBlazorComponents.Tests/SuperDataGridExporterTests.cs +++ b/tests/SuperBlazorComponents.Tests/SuperDataGridExporterTests.cs @@ -40,12 +40,16 @@ public void Setup() public void Cleanup() { foreach (var context in _contexts) + { context.Dispose(); + } _contexts.Clear(); _renderedGrids.Clear(); if (Directory.Exists(_temporaryDirectory)) + { Directory.Delete(_temporaryDirectory, recursive: true); + } } [TestMethod] @@ -76,6 +80,31 @@ public void ColumnResolver_UsesConfiguredPriorityAndExportValue() Assert.IsTrue(column.IsCurrentlyVisible); } + [TestMethod] + public void ColumnResolver_UsesTitleAndRenderedCellTemplateWithoutBinding() + { + var grid = CreateGrid((request) => + ValueTask.FromResult(GridItemsProviderResult.Empty())); + var column = AddColumn(grid, new TestColumn + { + Title = "Fournisseurs", + Template = item => builder => + { + builder.OpenElement(0, "div"); + builder.AddContent(1, item.Name); + builder.CloseElement(); + } + }); + + var resolved = ExportColumnResolver.Resolve(grid); + + Assert.HasCount(1, resolved); + Assert.AreEqual("Fournisseurs", resolved[0].Header); + Assert.AreEqual(string.Empty, column.Property); + Assert.AreEqual("Alpha", resolved[0].ValueAccessor( + new TestRow(1, "Alpha", 12.5m, true))); + } + [TestMethod] public void ColumnResolver_ExtractsSimpleHeaderTemplateAndRejectsMissingValue() {