Skip to content

DataGrid: Export: Architecture, csv exporter, external binary data exporter #6067

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
21 changes: 21 additions & 0 deletions Blazorise.sln
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Blazorise.Weavers", "Source
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Blazorise.Weavers.Fody", "Source\SourceGenerators\Blazorise.Weavers.Fody\Blazorise.Weavers.Fody.csproj", "{FFC4A285-1A16-4DD4-8B8C-141521E405B0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Blazorise.Exporters.Bson", "Source\Extensions\Blazorise.Exporters.Bson\Blazorise.Exporters.Bson.csproj", "{01A482C0-8DD8-4A9D-95FF-5CC2F71DB41B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Blazorise.Exporters.Csv", "Source\Extensions\Blazorise.Exporters.Csv\Blazorise.Exporters.Csv.csproj", "{1D465B0D-4905-438A-8581-A0657A602A33}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Blazorise.Exporters", "Source\Extensions\Blazorise.Exporters\Blazorise.Exporters.csproj", "{B5B0EB7F-3457-4E93-AF9A-DE864CBB21BE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -423,6 +429,18 @@ Global
{FFC4A285-1A16-4DD4-8B8C-141521E405B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FFC4A285-1A16-4DD4-8B8C-141521E405B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FFC4A285-1A16-4DD4-8B8C-141521E405B0}.Release|Any CPU.Build.0 = Release|Any CPU
{01A482C0-8DD8-4A9D-95FF-5CC2F71DB41B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{01A482C0-8DD8-4A9D-95FF-5CC2F71DB41B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{01A482C0-8DD8-4A9D-95FF-5CC2F71DB41B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{01A482C0-8DD8-4A9D-95FF-5CC2F71DB41B}.Release|Any CPU.Build.0 = Release|Any CPU
{1D465B0D-4905-438A-8581-A0657A602A33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1D465B0D-4905-438A-8581-A0657A602A33}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1D465B0D-4905-438A-8581-A0657A602A33}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1D465B0D-4905-438A-8581-A0657A602A33}.Release|Any CPU.Build.0 = Release|Any CPU
{B5B0EB7F-3457-4E93-AF9A-DE864CBB21BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B5B0EB7F-3457-4E93-AF9A-DE864CBB21BE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B5B0EB7F-3457-4E93-AF9A-DE864CBB21BE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B5B0EB7F-3457-4E93-AF9A-DE864CBB21BE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -495,6 +513,9 @@ Global
{EAB7EC89-900A-4280-B24A-152B9DD2B503} = {9731051E-0AA7-411E-A76A-987854F034DA}
{BF5FFB8C-45AD-4875-BB01-2DA388890419} = {0538DB67-B4F3-4D00-B969-D3874A52E405}
{FFC4A285-1A16-4DD4-8B8C-141521E405B0} = {0538DB67-B4F3-4D00-B969-D3874A52E405}
{01A482C0-8DD8-4A9D-95FF-5CC2F71DB41B} = {9731051E-0AA7-411E-A76A-987854F034DA}
{1D465B0D-4905-438A-8581-A0657A602A33} = {9731051E-0AA7-411E-A76A-987854F034DA}
{B5B0EB7F-3457-4E93-AF9A-DE864CBB21BE} = {9731051E-0AA7-411E-A76A-987854F034DA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {205B3EA4-470F-45DA-911E-346AF7D0A9A5}
Expand Down
2 changes: 2 additions & 0 deletions Demos/Blazorise.Demo/Blazorise.Demo.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
<ProjectReference Include="..\..\Source\Extensions\Blazorise.SignaturePad\Blazorise.SignaturePad.csproj" />
<ProjectReference Include="..\..\Source\Extensions\Blazorise.FluentValidation\Blazorise.FluentValidation.csproj" />
<ProjectReference Include="..\..\Source\Extensions\Blazorise.PdfViewer\Blazorise.PdfViewer.csproj" />
<ProjectReference Include="..\..\Source\Extensions\Blazorise.Exporters.Csv\Blazorise.Exporters.Csv.csproj" />
<ProjectReference Include="..\..\Source\Extensions\Blazorise.Exporters.Bson\Blazorise.Exporters.Bson.csproj" />
<ProjectReference Include="..\Apps\TodoApp\TodoApp.csproj" />
</ItemGroup>

Expand Down
7 changes: 7 additions & 0 deletions Demos/Blazorise.Demo/Pages/Tests/DataGrid/DataGridPage.razor
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@
<CardText>Combine diferent datagrid options</CardText>
</CardBody>
<CardBody>
<Row>
<Column>
<Button Color="Color.Primary" Clicked="@OnExport">
Export TEST
</Button>
</Column>
</Row>
<Row>
<Column>
<Fields>
Expand Down
20 changes: 20 additions & 0 deletions Demos/Blazorise.Demo/Pages/Tests/DataGrid/DataGridPage.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using System.Threading.Tasks;
using Blazorise.DataGrid;
using Blazorise.DataGrid.Utils;
using Blazorise.Exporters.Bson;
using Blazorise.Exporters.Csv;
using Blazorise.Shared.Data;
using Blazorise.Shared.Models;
using Microsoft.AspNetCore.Components;
Expand Down Expand Up @@ -251,5 +253,23 @@ private void OnSortChanged( DataGridSortChangedEventArgs eventArgs )
Console.WriteLine( $"Sort changed > Field: {eventArgs.ColumnFieldName}{sort}; Direction: {eventArgs.SortDirection};" );
}

private async Task OnExport()
{
// Simple export to CSV with default options
var result1 = await dataGrid.Export( new CsvToFileExporter() );

// Copy to clipboard without headers
var result2 = await dataGrid.Export( new CsvToClipboardExporter( new() { ExportHeader = false } ) );

// Export to CSV file with a custom file name and only the first 3 rows
var result3 = await dataGrid.Export(
new CsvToFileExporter( new() { FileName = "custom-csv-file.csv" } ),
new DataGridExportOptions { NumberOfRows = 3 }
);

// Bson export (defined in an external project)
var result4 = await dataGrid.Export( new BsonToFileExporter() );
}

#endregion
}
2 changes: 1 addition & 1 deletion Source/Blazorise/Base/BaseTypographyComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ protected override void BuildClasses( ClassBuilder builder )
protected async Task OnClickHandler()
{
if ( CopyToClipboard )
await JSUtilitiesModule.CopyToClipboard( ElementRef, ElementId );
await JSUtilitiesModule.CopyContentToClipboard( ElementRef, ElementId );
}

#endregion
Expand Down
19 changes: 18 additions & 1 deletion Source/Blazorise/Interfaces/Modules/IJSUtilitiesModule.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#region Using directives
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
#endregion

namespace Blazorise.Modules;
Expand Down Expand Up @@ -164,7 +165,14 @@ public interface IJSUtilitiesModule : IBaseJSModule
/// <param name="elementRef">Reference to the rendered element.</param>
/// <param name="elementId">ID of the rendered element.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
ValueTask CopyToClipboard( ElementReference elementRef, string elementId );
ValueTask CopyContentToClipboard( ElementReference elementRef, string elementId );

/// <summary>
/// Copies the specified string content to the clipboard.
/// </summary>
/// <param name="stringToCopy">The string content to copy to the clipboard.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
ValueTask CopyStringToClipboard( string stringToCopy );

/// <summary>
/// Writes a log message to the browser console.
Expand All @@ -179,4 +187,13 @@ public interface IJSUtilitiesModule : IBaseJSModule
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains true if the theme is in dark mode, otherwise false.</returns>
ValueTask<bool> IsSystemDarkMode();

/// <summary>
/// Exports data to a specified file with a given MIME type asynchronously.
/// </summary>
/// <param name="data">The byte array containing the data to be exported.</param>
/// <param name="fileName">The name of the file to which the data will be exported.</param>
/// <param name="mimeType">The MIME type that describes the format of the data being exported.</param>
/// <returns>An integer indicating the result of the export operation.</returns>
ValueTask<int> ExportToFile( byte[] data, string fileName, string mimeType );
}
10 changes: 9 additions & 1 deletion Source/Blazorise/Modules/JSUtilitiesModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,13 @@ public virtual ValueTask<string> GetUserAgent()
=> InvokeSafeAsync<string>( "getUserAgent" );

/// <inheritdoc/>
public ValueTask CopyToClipboard( ElementReference elementRef, string elementId )
public ValueTask CopyContentToClipboard( ElementReference elementRef, string elementId )
=> InvokeSafeVoidAsync( "copyToClipboard", elementRef, elementId );

/// <inheritdoc/>
public ValueTask CopyStringToClipboard( string stringToCopy )
=> InvokeSafeVoidAsync( "copyStringToClipboard", stringToCopy );

/// <inheritdoc/>
public ValueTask Log( string message, params string[] args )
=> InvokeSafeVoidAsync( "log", message, args );
Expand All @@ -119,6 +123,10 @@ public ValueTask Log( string message, params string[] args )
public ValueTask<bool> IsSystemDarkMode()
=> InvokeSafeAsync<bool>( "isSystemDarkMode" );

/// <inheritdoc/>
public virtual async ValueTask<int> ExportToFile( byte[] data, string fileName, string mimeType )
=> await InvokeSafeAsync<int>( "exportToFile", data, fileName, mimeType );

#endregion

#region Properties
Expand Down
32 changes: 32 additions & 0 deletions Source/Blazorise/wwwroot/utilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,12 @@ export function copyToClipboard(element, elementId) {
}
}

export function copyStringToClipboard(stringToCopy) {
if (navigator.clipboard) {
navigator.clipboard.writeText(stringToCopy);
}
}

function getExponentialParts(num) {
return Array.isArray(num) ? num : String(num).split(/[eE]/);
}
Expand Down Expand Up @@ -355,4 +361,30 @@ export function insertCSSIntoDocumentHead(url) {

export function isSystemDarkMode() {
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
}

export function exportToFile(data, fileName, mimeType) {
// Convert .NET byte array to Uint8Array
const uint8Array = new Uint8Array(data);

// Create Blob with specified MIME type
const blob = new Blob([uint8Array], { type: mimeType });

// Create temporary URL
const url = URL.createObjectURL(blob);

// Create hidden anchor element
const a = document.createElement('a');
a.href = url;
a.download = fileName;
document.body.appendChild(a);

// Trigger download
a.click();

// Cleanup
document.body.removeChild(a);
URL.revokeObjectURL(url);

return 1; // Success
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#region Using directives
using System;
using System.Threading.Tasks;
using Blazorise.Exporters;
using Blazorise.Extensions;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

<ItemGroup>
<ProjectReference Include="..\..\Blazorise\Blazorise.csproj" />
<ProjectReference Include="..\Blazorise.Exporters\Blazorise.Exporters.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Blazorise.DataGrid;

/// <summary>
/// Specifies the number of rows to export from a data grid.
/// </summary>
public class DataGridExportOptions
{
/// <summary>
/// -1 means all rows
/// </summary>
public int NumberOfRows { get; init; } = -1;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can add more options here

public string[] Fields { get; init; } // if null, then all fields are exported

public bool UseCaptions { get; init; } // if true, the Caption will be used for Csv column names

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a question. My idea was to keep it consistent with the "rich markup properties". Basically specify these in markup using column parameters - for example the BaseDataGridColumn.Func<object, object> ExportValue or ExportHeader , SupressExport or similar.

And keep the DataGridExportOptions only for the export itself (that cannot be specified per-column).
The ExportValue is an ultimate customization that would have to be there anyway. And keeping the "column export customization" inside DataGridExportOptions will duplicate the functionality in potentially confusing way.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think adding them in markup makes much since we are already having an Export() method that works in an "imperative" way. So it is only natural that we want to expand on DataGridExportOptions on what to actually export.

}
65 changes: 64 additions & 1 deletion Source/Extensions/Blazorise.DataGrid/DataGrid.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
using System.Collections.Specialized;
using System.Dynamic;
using System.Linq;
using System.Reflection.Metadata;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Blazorise.DataGrid.Utils;
using Blazorise.DeepCloner;
using Blazorise.Exporters;
using Blazorise.Extensions;
using Blazorise.Licensing;
using Blazorise.Modules;
Expand All @@ -25,7 +27,7 @@ namespace Blazorise.DataGrid;
/// </summary>
/// <typeparam name="TItem">Type parameter for the model displayed in the <see cref="DataGrid{TItem}"/>.</typeparam>
[CascadingTypeParameter( nameof( TItem ) )]
public partial class DataGrid<TItem> : BaseDataGridComponent
public partial class DataGrid<TItem> : BaseDataGridComponent, IExportableComponent
{
#region Members

Expand Down Expand Up @@ -488,6 +490,7 @@ protected override async Task OnAfterRenderAsync( bool firstRender )

IsClientMacintoshOS = await IsUserAgentMacintoshOS();
await JSModule.Initialize( tableRef.ElementRef, ElementId );

if ( IsCellNavigable )
{
await JSModule.InitializeTableCellNavigation( tableRef.ElementRef, ElementId );
Expand Down Expand Up @@ -1803,6 +1806,66 @@ public ValueTask ScrollToPixels( int pixels )
public ValueTask ScrollToRow( int row )
=> tableRef.ScrollToRow( row );

/// <summary>
/// Exports data using a specified exporter and options, returning the result of the export operation.
/// </summary>
/// <typeparam name="TExportResult">Defines the type of the result produced by the export operation.</typeparam>
/// <typeparam name="TCellValue">Specifies the type of the cell values in the data being exported.</typeparam>
/// <param name="exporter">An object responsible for handling the export process and generating the output.</param>
/// <param name="options">Configuration settings that influence the export behavior and output format.</param>
/// <returns>The result of the export operation, encapsulated in the specified result type.</returns>
public async Task<TExportResult> Export<TExportResult, TCellValue>( IExporter<TExportResult, TabularSourceData<TCellValue>> exporter, DataGridExportOptions options = null )
where TExportResult : IExportResult, new()
{
if ( exporter is IExporterWithJsModule exporterWithJsModule )
{
exporterWithJsModule.JSUtilitiesModule = JSUtilitiesModule;
}

var data = ExportData<TCellValue>( options );

TExportResult exportResult = await exporter.Export( data );

return exportResult;
}

private TabularSourceData<TCellValue> ExportData<TCellValue>( DataGridExportOptions options )
{
options ??= new();

// Filter columns (exclude Command, MultiSelect, and DisplayTemplate columns)
var columnsToExport = Columns
.Where( column => column.ColumnType != DataGridColumnType.Command && column.ColumnType != DataGridColumnType.MultiSelect && column.Field != null && column.DisplayTemplate == null )
.ToList();

var exportedData = new List<List<TCellValue>>();

var columnNames = columnsToExport.Select( c => c.Caption ).ToList();

var filteredDataToTake = options.NumberOfRows == -1 ? FilteredData : FilteredData.Take( options.NumberOfRows );

bool isCellValueString = typeof( TCellValue ) == typeof( string );

foreach ( var item in filteredDataToTake )
{
var rowValues = new List<TCellValue>();

foreach ( var column in columnsToExport )
{
var cellValue = column.GetValue( item );
object formattedValue = isCellValueString
? column.FormatDisplayValue( cellValue ) ?? ""
: cellValue;

rowValues.Add( (TCellValue)formattedValue );
}

exportedData.Add( rowValues );
}

return new TabularSourceData<TCellValue> { Data = exportedData, ColumnNames = columnNames };
}

#endregion

#region Editing
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<Import Project="..\..\..\Build\Blazorise.props" />

<PropertyGroup>
<PackageTags>blazorise blazor exporter bson</PackageTags>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\Blazorise\Blazorise.csproj" />
<ProjectReference Include="..\Blazorise.Exporters\Blazorise.Exporters.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="MongoDB.Bson" Version="3.3.0" />
</ItemGroup>

<ItemGroup>
<None Include="..\..\..\LICENSE.md" Pack="true" Visible="false" PackagePath="" />
<None Include="..\..\..\NuGet\Blazorise.png" Pack="true" Visible="false" PackagePath="" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Blazorise.Exporters.Bson;

/// <summary>
/// Options for exporting files in BSON format, including file extension and MIME type.
/// </summary>
public class BsonFileExportOptions : FileExportOptions
{
/// <summary>
/// Represents the file extension for the object, initialized to 'bson'.
/// </summary>
public override string FileName { get; init; } = "exported-data.bson";

/// <summary>
/// Represents the MIME type for BSON data format. It is initialized to 'application/bson'.
/// </summary>
public override string MimeType { get; init; } = "application/bson";

/// <summary>
/// Indicates whether type information should be included. Defaults to true.
/// </summary>
public bool IncludeTypeInformation { get; init; } = true;
}
Loading
Loading