Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions SUPERDATAGRIDEXPORTER.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<DataGridColumn Title="Suppliers">
<Template Context="product">
@foreach (var supplier in product.AvailableSupplierProductList)
{
<div>@supplier.SupplierName</div>
}
</Template>
</DataGridColumn>
~~~

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
<DataGridColumn Title="Status"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ public static IReadOnlyList<ExportColumn<TItem>> Resolve<TItem>(SuperDataGrid<TI
{
accessor = column.ExportValue;
}
else if (column.Template is not null)
{
var cellTemplate = column.Template;
accessor = item => ExtractCellText(cellTemplate, item);
}
else if (!string.IsNullOrWhiteSpace(column.Property))
{
var untypedAccessor = Accessors.GetOrAdd(
Expand All @@ -52,14 +57,16 @@ public static IReadOnlyList<ExportColumn<TItem>> Resolve<TItem>(SuperDataGrid<TI
else
{
throw new InvalidOperationException(
$"Column '{header}' has no resolvable value. Set Property, For, or ExportValue on the column.");
$"Column '{header}' has no resolvable value. Set a Template, Property, For, or ExportValue on the column.");
}

result.Add(new ExportColumn<TItem>(header, column.FormatString, accessor));
}

if (result.Count == 0)
{
throw new InvalidOperationException("The grid has no visible exportable columns.");
}

return result;
}
Expand All @@ -75,7 +82,9 @@ public static IReadOnlyList<ExportColumn<TItem>> Resolve<TItem>(SuperDataGrid<TI
?? currentType.GetField(segment, BindingFlags.Instance | BindingFlags.Public);

if (member is null)
{
throw new InvalidOperationException($"Property path '{propertyPath}' was not found on '{itemType.Name}'.");
}

members.Add(member);
currentType = member switch
Expand All @@ -92,7 +101,9 @@ public static IReadOnlyList<ExportColumn<TItem>> Resolve<TItem>(SuperDataGrid<TI
foreach (var member in members)
{
if (value is null)
{
return null;
}
value = member switch
{
PropertyInfo property => property.GetValue(value),
Expand All @@ -105,23 +116,51 @@ public static IReadOnlyList<ExportColumn<TItem>> Resolve<TItem>(SuperDataGrid<TI
}

#pragma warning disable BL0006 // Deliberately inspect simple text frames; component output falls back to Title/Property.
private static string? ExtractCellText<TItem>(RenderFragment<TItem> 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<RenderTreeFrame> frames)
{
var parts = new List<string>();

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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<Nullable>enable</Nullable>
<RootNamespace>SuperBlazorComponents.DataGridExporter</RootNamespace>
<AssemblyName>SuperBlazorComponents.DataGridExporter</AssemblyName>
<VersionPrefix>2.0.10</VersionPrefix>
<VersionPrefix>2.0.12</VersionPrefix>
<PackageId>SuperBlazorComponents.DataGridExporter</PackageId>
<Title>SuperBlazorComponents DataGrid Exporter</Title>
<Authors>Appliman</Authors>
Expand Down
29 changes: 29 additions & 0 deletions tests/SuperBlazorComponents.Tests/SuperDataGridExporterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -76,6 +80,31 @@ public void ColumnResolver_UsesConfiguredPriorityAndExportValue()
Assert.IsTrue(column.IsCurrentlyVisible);
}

[TestMethod]
public void ColumnResolver_UsesTitleAndRenderedCellTemplateWithoutBinding()
{
var grid = CreateGrid((request) =>
ValueTask.FromResult(GridItemsProviderResult<TestRow>.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()
{
Expand Down
Loading