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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
| Preview | Component | Description | Docs |
|---|---|---|---|
| <img src="docs/images/components/superdatagrid.svg" width="88" alt="SuperDataGrid preview"> | **SuperDataGrid** | Virtualized data grid — frozen columns/rows, hierarchical lazy-loading rows, reordering, resizing, filtering, sorting, inline editing, row selection, settings persistence | [📖 SUPERDATAGRID.md](SUPERDATAGRID.md) |
| <img src="docs/images/components/superdatagrid.svg" width="88" alt="SuperDataGrid exporter preview"> | **SuperDataGrid Exporter** | Optional CSV and Excel export extension for complete filtered, sorted and virtualized datasets | [📖 SUPERDATAGRIDEXPORTER.md](SUPERDATAGRIDEXPORTER.md) |
| <img src="docs/images/components/superdatagrid.svg" width="88" alt="SuperDataGrid exporter preview"> | **SuperDataGrid Exporter** | Optional CSV and Excel export extension for checked rows, including filtered, sorted and virtualized data | [📖 SUPERDATAGRIDEXPORTER.md](SUPERDATAGRIDEXPORTER.md) |
| <img src="docs/images/components/superlayout.svg" width="88" alt="SuperLayout preview"> | **SuperLayout** | Responsive app layout — header, sidebar, body, footer, chat panel with collapsible sidebar | [📖 SUPERLAYOUT.md](SUPERLAYOUT.md) |
| <img src="docs/images/components/supercontext.svg" width="88" alt="SuperContext preview"> | **SuperContext** | Context-aware tabs and contextual panels — discover components by runtime type and zone, render one or many contexts, and isolate host state per instance | [📖 SUPERCONTEXT.md](SUPERCONTEXT.md) |
| <img src="docs/images/components/supertabs.svg" width="88" alt="SuperTabs preview"> | **SuperTabs** | Dynamic tabbed interface — badges, closable tabs, lazy loading, persistence (URL + localStorage), keyboard navigation, service-driven management | [📖 SUPERTABS.md](SUPERTABS.md) |
Expand Down
34 changes: 24 additions & 10 deletions SUPERDATAGRIDEXPORTER.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# SuperDataGrid CSV and Excel exporter

The **SuperBlazorComponents.DataGridExporter** extension exports the complete
filtered and sorted view of a **SuperDataGrid**, including rows that are not
currently rendered by virtualization.
The **SuperBlazorComponents.DataGridExporter** extension exports the rows that
are checked in a **SuperDataGrid**. A checked row is captured when generation
starts, so it remains exportable even if a later filter hides it.

## Installation

Expand Down Expand Up @@ -74,11 +74,23 @@ actions to the right of the grid header. The grid keeps the custom header area
separated from its built-in actions. Omit **IconOnly** to display the icon and
text together.

Only currently visible columns are exported, in their current order. The
exporter captures the grid filters and sort order once, then reads the complete
result from **ItemsProvider** in batches. Hierarchical grids export root items
only. The default batch size is 200 rows and can be changed with
**SuperDataGridExporterOptions.BatchSize**.
Only currently visible columns are exported, in their current order. If rows
are selected individually, those exact objects are exported in selection order.
If **Tout sélectionner** is used, the exporter reads all rows matching the
captured filters and sort order from **ItemsProvider**, in batches of 200 by
default, and skips rows explicitly unchecked afterwards. The batch size can be
changed with **SuperDataGridExporterOptions.BatchSize**.

The dialog is still opened when nothing is checked, but immediately displays:
“Veuillez cocher au moins une ligne pour effectuer l’export.” Check a row and
choose **Réessayer** to continue; no file is created while the selection is
empty. Selection is frozen at the start of generation.

For hierarchical grids, every checked row is exported, including checked child
rows. Unchecked or unexpanded children are not invented by the exporter.
Virtualized providers should implement **IDataItem.KeyValue** with a stable,
unique key. The same key is used to apply exclusions and deduplicate rows when
the provider materializes a new object instance for each batch.

## Custom columns

Expand Down Expand Up @@ -120,5 +132,7 @@ Set **Exportable="false"** to exclude a visible column.
sizes columns to their content. Excel cannot freeze columns from the right.
- One worksheet supports at most 1,048,575 exported data rows because the
header occupies the first Excel row.
- The grid must use **ItemsProvider**; the currently rendered virtualized items
alone are intentionally never treated as the complete dataset.
- **ItemsProvider** is required for **Tout sélectionner** so the exporter can
retrieve every filtered row; it never treats the currently rendered
virtualized items alone as the complete dataset. Individually checked objects
are exported from the immutable selection captured at the start.
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@
private bool _initialized;
private SuperDataGridExportResult? _result;

private const string SelectionRequiredMessage =
"Veuillez cocher au moins une ligne pour effectuer l’export.";

private string Extension => Format == SuperDataGridExportFormat.Csv ? ".csv" : ".xlsx";

protected override void OnParametersSet()
Expand All @@ -75,6 +78,8 @@
_fileName = string.IsNullOrWhiteSpace(DefaultFileName)
? $"export-{DateTime.Now:yyyyMMdd-HHmmss}"
: DefaultFileName;
if (!Grid.CaptureSelectionSnapshot().HasSelection)
_error = SelectionRequiredMessage;
_initialized = true;
}

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.8</VersionPrefix>
<VersionPrefix>2.0.9</VersionPrefix>
<PackageId>SuperBlazorComponents.DataGridExporter</PackageId>
<Title>SuperBlazorComponents DataGrid Exporter</Title>
<Authors>Appliman</Authors>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,18 @@ public Task<SuperDataGridExportResult> ExportAsync<TItem>(
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(grid);
if (grid.ItemsProvider is null)
throw new InvalidOperationException("The grid must define an ItemsProvider to export all rows.");

// Capture both parts of the view before starting any asynchronous work. This
// makes an export deterministic even when the user changes the grid while it
// is being generated.
var selection = grid.CaptureSelectionSnapshot();
if (!selection.HasSelection)
throw new InvalidOperationException(
"Veuillez cocher au moins une ligne pour effectuer l’export.");

if (selection.AllSelected && grid.ItemsProvider is null)
throw new InvalidOperationException(
"The grid must define an ItemsProvider to export all selected rows.");

var columns = ExportColumnResolver.Resolve(grid);
var query = grid.CaptureQuerySnapshot();
Expand All @@ -47,15 +57,17 @@ public Task<SuperDataGridExportResult> ExportAsync<TItem>(
format,
fileName,
(path, token) => format == SuperDataGridExportFormat.Csv
? WriteCsvAsync(path, grid.ItemsProvider, query, columns, token)
: WriteExcelAsync(path, grid.ItemsProvider, query, columns, frozenColumnCount, token),
? WriteCsvAsync(path, grid, grid.ItemsProvider, query, selection, columns, token)
: WriteExcelAsync(path, grid, grid.ItemsProvider, query, selection, columns, frozenColumnCount, token),
cancellationToken);
}

private async Task<int> WriteCsvAsync<TItem>(
string path,
GridItemsProvider<TItem> provider,
SuperDataGrid<TItem> grid,
GridItemsProvider<TItem>? provider,
SuperDataGridQuerySnapshot query,
SuperDataGridSelectionSnapshot<TItem> selection,
IReadOnlyList<ExportColumn<TItem>> columns,
CancellationToken cancellationToken)
{
Expand All @@ -75,7 +87,7 @@ private async Task<int> WriteCsvAsync<TItem>(
await csv.NextRecordAsync();

var rowCount = 0;
await foreach (var item in ReadAllAsync(provider, query, cancellationToken))
await foreach (var item in ReadSelectedAsync(grid, provider, query, selection, cancellationToken))
{
foreach (var column in columns)
{
Expand All @@ -94,8 +106,10 @@ private async Task<int> WriteCsvAsync<TItem>(

private async Task<int> WriteExcelAsync<TItem>(
string path,
GridItemsProvider<TItem> provider,
SuperDataGrid<TItem> grid,
GridItemsProvider<TItem>? provider,
SuperDataGridQuerySnapshot query,
SuperDataGridSelectionSnapshot<TItem> selection,
IReadOnlyList<ExportColumn<TItem>> columns,
int frozenColumnCount,
CancellationToken cancellationToken)
Expand All @@ -111,7 +125,7 @@ private async Task<int> WriteExcelAsync<TItem>(
}

var rowCount = 0;
await foreach (var item in ReadAllAsync(provider, query, cancellationToken))
await foreach (var item in ReadSelectedAsync(grid, provider, query, selection, cancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
if (rowCount >= ExcelMaximumDataRows)
Expand All @@ -138,6 +152,61 @@ private async Task<int> WriteExcelAsync<TItem>(
return rowCount;
}

private async IAsyncEnumerable<TItem> ReadSelectedAsync<TItem>(
SuperDataGrid<TItem> grid,
GridItemsProvider<TItem>? provider,
SuperDataGridQuerySnapshot query,
SuperDataGridSelectionSnapshot<TItem> selection,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
var emittedKeys = new HashSet<object?>();

if (!selection.AllSelected)
{
// Keep the order in which the grid captured individual selections. The
// objects themselves are deliberately used so that a later filter or
// provider refresh cannot make a checked row disappear from the export.
foreach (var item in selection.SelectedItems)
{
cancellationToken.ThrowIfCancellationRequested();
if (emittedKeys.Add(grid.GetItemKey(item)))
yield return item;
}

yield break;
}

if (provider is null)
throw new InvalidOperationException(
"The grid must define an ItemsProvider to export all selected rows.");

// Select-all means all rows in the current filtered/sorted view, except the
// explicitly excluded keys. ItemsProvider is paged so virtualized grids are
// exported in full rather than only using the rendered viewport.
await foreach (var item in ReadAllAsync(provider, query, cancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
var key = grid.GetItemKey(item);
if (selection.ExcludedItemKeys.Contains(key) || !emittedKeys.Add(key))
continue;

yield return item;
}

// Explicitly checked rows (notably hierarchy children) are retained even if
// they are not part of the root ItemsProvider result or became hidden by a
// later filter. Stable keys still deduplicate them against provider rows.
foreach (var item in selection.SelectedItems)
{
cancellationToken.ThrowIfCancellationRequested();
var key = grid.GetItemKey(item);
if (selection.ExcludedItemKeys.Contains(key) || !emittedKeys.Add(key))
continue;

yield return item;
}
}

private async IAsyncEnumerable<TItem> ReadAllAsync<TItem>(
GridItemsProvider<TItem> provider,
SuperDataGridQuerySnapshot query,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ public sealed class SelectionInfo<TItem>
{
public HashSet<TItem> SelectedItems { get; } = [];

// HashSet is kept for fast membership checks and backwards compatibility. The
// list preserves the order in which individual rows were checked for exports.
internal List<TItem> SelectionOrder { get; } = [];

internal HashSet<object?> UnselectedItemKeys { get; } = [];

public int TotalCount { get; set; }
Expand All @@ -14,5 +18,25 @@ public sealed class SelectionInfo<TItem>

public int ExcludedCount { get; set; }

public int SelectedCountTotal => SelectedCount - ExcludedCount;
public int SelectedCountTotal => Math.Max(0, SelectedCount - ExcludedCount);

internal void AddSelected(TItem item)
{
if (SelectedItems.Add(item))
SelectionOrder.Add(item);
}

internal bool RemoveSelected(TItem item)
{
var removed = SelectedItems.Remove(item);
if (removed)
SelectionOrder.Remove(item);
return removed;
}

internal void ClearSelected()
{
SelectedItems.Clear();
SelectionOrder.Clear();
}
}
Loading
Loading