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
3 changes: 2 additions & 1 deletion GridMasterDetailExport/Components/App.razor
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
@DxResourceManager.RegisterTheme(FluentLight)
@DxResourceManager.RegisterScripts()
<link href=@AppendVersion("css/site.css") rel="stylesheet" />
<link href=@AppendVersion("BlazorBlankServer.styles.css") rel="stylesheet" />
<link href=@AppendVersion("GridMasterDetailExport.styles.css") rel="stylesheet" />
<HeadOutlet />
<script type="text/javascript" src="scripts/DownloadHelper.js"></script>
</head>
<body class="dxbl-theme-fluent">
<Routes />
Expand Down
5 changes: 2 additions & 3 deletions GridMasterDetailExport/Components/Pages/Error.razor
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>

@if (ShowRequestId)
{
@if(ShowRequestId) {
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
</p>
Expand All @@ -24,7 +23,7 @@
and restarting the app.
</p>

@code{
@code {
[CascadingParameter]
private HttpContext? HttpContext { get; set; }

Expand Down
107 changes: 103 additions & 4 deletions GridMasterDetailExport/Components/Pages/Index.razor
Original file line number Diff line number Diff line change
@@ -1,6 +1,105 @@
@page "/"
<PageTitle>Welcome</PageTitle>
@rendermode InteractiveServer
@inject IJSRuntime JS
@using DevExpress.XtraReports.UI
@using GridMasterDetailExport.Data
@using GridMasterDetailExport.Reports

<div class="main-content">
Welcome to your new DevExpress Blazor Application
</div>
<DxGrid @ref="MasterGrid"
AllowColumnReorder="false"
AllowSort="false"
Data="DataProvider.GetUsers()"
KeyFieldName="UserID">
<ToolbarTemplate Context="ctx">
<DxToolbar>
<Items>
<DxToolbarItem Text="Export as PDF" Click="ExportToPdf" />
<DxToolbarItem Text="Export as XLSX" Click="ExportToXlsx" />
<DxToolbarItem Text="Export as CSV" Click="ExportToCsv" />
</Items>
</DxToolbar>
</ToolbarTemplate>
<Columns>
<DxGridDataColumn FieldName="UserID" />
<DxGridDataColumn FieldName="UserName" />
</Columns>
<DetailRowTemplate Context="ctx">
@{
User user = ctx.DataItem as User ?? new();
var userOrders =
DataProvider.GetOrders()
.Where(o => o.UserID == user.UserID);
}
<DxGrid @ref="DetailGrid"
AllowColumnReorder="false"
AllowSort="false"
Data="userOrders"
KeyFieldName="OrderID">
<Columns>
<DxGridDataColumn FieldName="OrderID" />
<DxGridDataColumn FieldName="OrderDate" />
<DxGridDataColumn FieldName="ProductName" />
</Columns>
</DxGrid>
</DetailRowTemplate>
</DxGrid>

@code {
public DxGrid? MasterGrid { get; set; }

public DxGrid? DetailGrid { get; set; }

private async Task ExportToPdf() {
using var report = GetReport();
using var stream = new MemoryStream();
await report.ExportToPdfAsync(stream);
await DownloadStreamAsync(stream, "exportResult.pdf");
}

private async Task ExportToXlsx() {
using var report = GetReport();
using var stream = new MemoryStream();
await report.ExportToXlsxAsync(stream);
await DownloadStreamAsync(stream, "exportResult.xlsx");
}

private async Task ExportToCsv() {
using var report = GetReport();
using var stream = new MemoryStream();
await report.ExportToCsvAsync(stream);
await DownloadStreamAsync(stream, "exportResult.csv");
}

private XtraReport GetReport() {
return ReportGenerationHelpers.GetReportFromDxGrid(
GetVisibleColumnNames(MasterGrid),
GetVisibleColumnNames(DetailGrid),
GetRowsExpandedStates()
);
}

private static string[] GetVisibleColumnNames(IGrid? grid) {
return grid?.GetVisibleColumns()
.OfType<DxGridDataColumn>()
.Select(c => c.FieldName ?? c.Name).ToArray() ?? [];
}

private Dictionary<string, bool> GetRowsExpandedStates() {
Dictionary<string, bool> masterRowExpandedStates = new();
for(int i = 0; i < MasterGrid?.GetVisibleRowCount(); i++) {
if(MasterGrid.GetDataItem(i) is User user) {
masterRowExpandedStates[user.UserID.ToString()]
= MasterGrid.IsDetailRowExpanded(i);
}
}
return masterRowExpandedStates;
}

async Task DownloadStreamAsync(Stream stream, string fileName) {

stream.Seek(0, SeekOrigin.Begin);

using var streamRef = new DotNetStreamReference(stream);
await JS.InvokeVoidAsync("downloadFileFromStream", fileName, streamRef);
}
}
27 changes: 27 additions & 0 deletions GridMasterDetailExport/Data/DataProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace GridMasterDetailExport.Data;
public class DataProvider {
private static List<User> users = [
new User(){ UserID = 1, UserName = "Alex" },
new User(){ UserID = 2, UserName = "Kevin" },
new User(){ UserID = 3, UserName = "Rose" },
];

private static List<Order> orders = [
new Order(){ OrderID = 1, UserID = 1, OrderDate = DateTime.Today.AddDays(1), ProductName = "Book" },
new Order(){ OrderID = 2, UserID = 1, OrderDate = DateTime.Today.AddDays(2), ProductName = "Sugar" },
new Order(){ OrderID = 3, UserID = 1, OrderDate = DateTime.Today.AddDays(3), ProductName = "Laptop" },
new Order(){ OrderID = 4, UserID = 2, OrderDate = DateTime.Today.AddDays(1), ProductName = "Brick" },
new Order(){ OrderID = 5, UserID = 2, OrderDate = DateTime.Today.AddDays(2), ProductName = "Stone" },
new Order(){ OrderID = 6, UserID = 2, OrderDate = DateTime.Today.AddDays(3), ProductName = "Potato" },
new Order(){ OrderID = 7, UserID = 2, OrderDate = DateTime.Today.AddDays(4), ProductName = "Cucumber" },
new Order(){ OrderID = 8, UserID = 2, OrderDate = DateTime.Today.AddDays(5), ProductName = "Keyboard" },
new Order(){ OrderID = 9, UserID = 3, OrderDate = DateTime.Today.AddDays(6), ProductName = "Pencil" },
new Order(){ OrderID = 10, UserID = 3, OrderDate = DateTime.Today.AddDays(7), ProductName = "Mug" },
new Order(){ OrderID = 11, UserID = 3, OrderDate = DateTime.Today.AddDays(8), ProductName = "Magnet" },
new Order(){ OrderID = 12, UserID = 3, OrderDate = DateTime.Today.AddDays(9), ProductName = "Vitamins" },
];

public static List<User> GetUsers() => users;

public static List<Order> GetOrders() => orders;
}
7 changes: 7 additions & 0 deletions GridMasterDetailExport/Data/Order.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace GridMasterDetailExport.Data;
public class Order {
public int OrderID { get; set; }
public DateTime OrderDate { get; set; }
public int UserID { get; set; }
public string? ProductName { get; set; }
}
5 changes: 5 additions & 0 deletions GridMasterDetailExport/Data/User.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace GridMasterDetailExport.Data;
public class User {
public int UserID { get; set; }
public string? UserName { get; set; }
}
2 changes: 2 additions & 0 deletions GridMasterDetailExport/GridMasterDetailExport.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DevExpress.Blazor" Version="25.1.*" />
<PackageReference Include="DevExpress.Drawing.Skia" Version="25.1.*" />
<PackageReference Include="DevExpress.Reporting.Core" Version="25.1.*" />
</ItemGroup>
</Project>
203 changes: 203 additions & 0 deletions GridMasterDetailExport/Reports/ReportGenerationHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
using System.Drawing;
using System.Text.Json;
using System.Text.Json.Serialization;
using DevExpress.DataAccess;
using DevExpress.DataAccess.DataFederation;
using DevExpress.DataAccess.ObjectBinding;
using DevExpress.Drawing;
using DevExpress.Drawing.Printing;
using DevExpress.XtraPrinting;
using DevExpress.XtraReports.UI;
using GridMasterDetailExport.Data;

namespace GridMasterDetailExport.Reports {
public static class ReportGenerationHelpers {
public static XtraReport GetReportFromDxGrid(string[] masterColumnNames, string[] detailColumnNames, Dictionary<string, bool> rowExpandedStates) {
XtraReport report = new XtraReport() {
ReportUnit = ReportUnit.HundredthsOfAnInch,
PaperKind = DXPaperKind.Letter,
Margins = new DXMargins(100, 100, 100, 100)
};

#region Styles
var tableHeaderStyle = new XRControlStyle {
Name = "tableHeaderStyle",
TextAlignment = TextAlignment.MiddleCenter,
BackColor = Color.AliceBlue,
};
report.StyleSheet.Add(tableHeaderStyle);

var tableRowStyle = new XRControlStyle() {
Name = "tableRowStyle",
TextAlignment = TextAlignment.MiddleCenter
};
report.StyleSheet.Add(tableRowStyle);
#endregion

#region DataSource

var masterDataSource = new ObjectDataSource() {
Name = "Users",
DataSource = typeof(DataProvider),
DataMember = nameof(DataProvider.GetUsers)
};
masterDataSource.RebuildResultSchema();

var detailDataSource = new ObjectDataSource() {
Name = "Orders",
DataSource = typeof(DataProvider),
DataMember = nameof(DataProvider.GetOrders)
};
detailDataSource.RebuildResultSchema();

var fds = ReportGenerationHelpers.CreateFederationDataSource(masterDataSource, detailDataSource, nameof(User.UserID), nameof(Order.UserID), rowExpandedStates);
#endregion

float contentWidth = report.PageWidthF - report.Margins.Left - report.Margins.Right;

report.DataSource = fds;
report.DataMember = masterDataSource.Name;

report.AddTableHeaderBand(new RectangleF(0f, 0f, contentWidth, 25f), tableHeaderStyle.Name, masterColumnNames);
report.AddTableRowBand(new RectangleF(0f, 0f, contentWidth, 25f), tableRowStyle.Name, masterColumnNames, true);
report.AddDetailReport(
$"{masterDataSource.Name}.{masterDataSource.Name}{detailDataSource.Name}",
detailColumnNames,
new RectangleF(20f, 20f, contentWidth - 20f, 25f),
tableHeaderStyle.Name,
tableRowStyle.Name);

return report;
}

static FederationDataSource CreateFederationDataSource(DataComponentBase masterDataSource, DataComponentBase detailDataSource, string parentKeyColumn, string nestedKeyColumn, Dictionary<string, bool> expandedStates) {
ObjectDataSource expandedStateDataSource = new ObjectDataSource() {
Name = "ExpandedState",
DataSource = typeof(ExpandedState),
DataMember = nameof(ExpandedState.LoadExpandedStates)
};
expandedStateDataSource.Parameters.Add(new Parameter(
"json",
typeof(string),
JsonSerializer.Serialize(expandedStates.Select((i) => new ExpandedState() { RowKey = i.Key, IsExpanded = i.Value }))));

Source masterSource = new Source(masterDataSource.Name, masterDataSource);
Source detailSource = new Source(detailDataSource.Name, detailDataSource);
Source expandedStatesSource = new Source("ExpandedState", expandedStateDataSource);


var masterQuery = masterSource.From()
.SelectAll()
.InnerJoin(expandedStatesSource, $"[{masterSource.Name}.{parentKeyColumn}] = [{expandedStatesSource.Name}].[{nameof(ExpandedState.RowKey)}]")
.Select(nameof(ExpandedState.IsExpanded))
.Build(masterSource.Name);
var detailQuery = detailSource.From()
.SelectAll()
.Build(detailSource.Name);

var federationDataSource = new FederationDataSource();
federationDataSource.Queries.AddRange([masterQuery, detailQuery]);

var relation = new FederationMasterDetailInfo(masterSource.Name, detailSource.Name, new FederationRelationColumnInfo(parentKeyColumn, nestedKeyColumn));
federationDataSource.Relations.Add(relation);
federationDataSource.RebuildResultSchema();

return federationDataSource;
}

public static void AddTableHeaderBand(this XtraReportBase report, RectangleF rect, string styleName, string[] columnCaptions) {
var headerBand = new GroupHeaderBand() {
HeightF = rect.Height,
RepeatEveryPage = true,
KeepTogether = true,
GroupUnion = GroupUnion.WithFirstDetail
};
report.Bands.Add(headerBand);

headerBand.AddXRTable(columnCaptions, rect, styleName,
(table) => {
table.Borders = BorderSide.All;
},
(cell, caption) => {
cell.Text = caption;
});
}

public static void AddTableRowBand(this XtraReportBase report, RectangleF rect, string styleName, string[] fieldNames, bool isExpandable = false) {
var rowBand = new DetailBand() {
HeightF = rect.Height,
KeepTogether = true,
KeepTogetherWithDetailReports = true
};
report.Bands.Add(rowBand);

rowBand.AddXRTable(fieldNames, rect, styleName,
(table) => {
if(isExpandable) {
table.ExpressionBindings.Add(new ExpressionBinding("BeforePrint", "Borders", $"IIf([DataSource.IsFirstRow] || !PrevRowColumnValue('{nameof(ExpandedState.IsExpanded)}'),'Left,Right,Bottom', 'All')\r\n"));
}
else {
table.Borders = BorderSide.Left | BorderSide.Right | BorderSide.Bottom;
}
},
(cell, fieldName) => {
cell.ExpressionBindings.Add(
new("BeforePrint", "Text", $"[{fieldName}]"));
});
}

public static void AddEmptyTableFooterBand(this XtraReportBase report, float height) {
var footerBand = new GroupFooterBand() {
HeightF = height,
GroupUnion = GroupFooterUnion.WithLastDetail
};
report.Bands.Add(footerBand);
footerBand.Controls.Add(new XRPanel() { HeightF = height });
}

public static void AddDetailReport(this XtraReportBase report, string dataMember, string[] columns, RectangleF rect, string headerStyleName, string rowStyleName) {
var detailReportBand = new DetailReportBand() {
DataSource = report.DataSource,
DataMember = dataMember
};
report.Bands.Add(detailReportBand);
detailReportBand.ExpressionBindings.Add(new ExpressionBinding("BeforePrint", "Visible", $"[{nameof(ReportGenerationHelpers.ExpandedState.IsExpanded)}]"));

detailReportBand.AddTableHeaderBand(rect, headerStyleName, columns);
detailReportBand.AddTableRowBand(new RectangleF(rect.X, 0f, rect.Width, rect.Height), rowStyleName, columns);
detailReportBand.AddEmptyTableFooterBand(rect.Height);
}

private static void AddXRTable<T>(this Band band, T[] columns, RectangleF rect, string styleName, Action<XRTable> tableInitializer, Action<XRTableCell, T> cellInitializer) {
var table = new XRTable();
band.Controls.Add(table);
table.BeginInit();
table.LocationF = rect.Location;
table.SizeF = rect.Size;
table.StyleName = styleName;
tableInitializer(table);

var row = new XRTableRow();
table.Rows.Add(row);

foreach(T column in columns) {
XRTableCell cell = new XRTableCell();
row.Cells.Add(cell);
cellInitializer(cell, column);
}

table.EndInit();
}

public class ExpandedState {
[JsonRequired]
public string RowKey { get; set; } = "";
[JsonRequired]
public bool IsExpanded { get; set; }

public static IEnumerable<ExpandedState>? LoadExpandedStates(string json) {
return JsonSerializer.Deserialize<List<ExpandedState>>(json);
}
}
}
}
Loading
Loading