Devexpress Grid ve Form raporlarını Richtext ve Image gösterilmesi

This commit is contained in:
Sedat ÖZTÜRK 2026-06-17 11:46:18 +03:00
parent 1997a8aacb
commit 2376eb641a
8 changed files with 1589 additions and 361 deletions

View file

@ -5,11 +5,13 @@ using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using DevExpress.DataAccess.ConnectionParameters;
using DevExpress.DataAccess.Sql;
using DevExpress.Drawing;
using DevExpress.XtraPrinting;
using DevExpress.XtraReports.UI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Sozsoft.Platform;
@ -36,10 +38,10 @@ public class DynamicFormReport : XtraReport
_serviceProvider = serviceProvider;
}
public static DynamicFormReport Create(IDictionary<string, string> parameters)
public static async Task<DynamicFormReport> CreateAsync(IDictionary<string, string> parameters)
{
var report = new DynamicFormReport();
report.Build(parameters);
await report.BuildAsync(parameters);
return report;
}
@ -47,7 +49,7 @@ public class DynamicFormReport : XtraReport
{
}
private void Build(IDictionary<string, string> parameters)
private async Task BuildAsync(IDictionary<string, string> parameters)
{
if (_serviceProvider == null)
{
@ -73,23 +75,22 @@ public class DynamicFormReport : XtraReport
var selectQueryManager = scope.ServiceProvider.GetRequiredService<ISelectQueryManager>();
var defaultValueHelper = scope.ServiceProvider.GetRequiredService<DefaultValueHelper>();
var localizer = scope.ServiceProvider.GetRequiredService<IStringLocalizer<PlatformResource>>();
var configuration = scope.ServiceProvider.GetRequiredService<IConfiguration>();
if (!authManager.CanAccess(listFormCode).GetAwaiter().GetResult())
if (!await authManager.CanAccess(listFormCode))
{
throw new UnauthorizedAccessException($"No read permission for list form '{listFormCode}'.");
}
var listForm = listFormManager.GetUserListForm(listFormCode).GetAwaiter().GetResult();
var fields = listFormFieldManager.GetUserListFormFields(listFormCode).GetAwaiter().GetResult()
var listForm = await listFormManager.GetUserListForm(listFormCode);
var fields = (await listFormFieldManager.GetUserListFormFields(listFormCode))
.Where(x => x.CanRead)
.OrderBy(x => x.ListOrderNo ?? int.MaxValue)
.ThenBy(x => x.CaptionName)
.ToList();
var customizations = customizationManager.GetUsersServerCustomizations(listFormCode).GetAwaiter().GetResult();
var (dynamicDataRepository, connectionString, dataSourceType) = dynamicDataManager
.GetAsync(listForm.IsTenant, listForm.DataSourceCode)
.GetAwaiter()
.GetResult();
var customizations = await customizationManager.GetUsersServerCustomizations(listFormCode);
var (dynamicDataRepository, connectionString, dataSourceType) = await dynamicDataManager
.GetAsync(listForm.IsTenant, listForm.DataSourceCode);
var queryParams = new QueryParameters
{
@ -102,19 +103,18 @@ public class DynamicFormReport : XtraReport
selectQueryManager.PrepareQueries(listForm, fields, dataSourceType, customizations, queryParams);
var reportFields = GetReportFields(listForm, fields, selectQueryManager, defaultValueHelper);
var selectQuery = ApplyLookupQueryColumns(
selectQueryManager.SelectQuery,
listForm,
var selectQuery = StripReportPaging(ApplyQueryParameters(selectQueryManager.SelectQuery, selectQueryManager.SelectQueryParameters));
selectQuery = DynamicReportSqlBuilder.ApplyLookupTextColumns(
selectQuery,
reportFields,
defaultValueHelper);
selectQuery = ApplyQueryParameters(selectQuery, selectQueryManager.SelectQueryParameters);
selectQuery = StripReportPaging(selectQuery);
defaultValueHelper,
dataSourceType);
var row = GetFirstRow(dynamicDataRepository.QueryAsync(selectQuery, connectionString).GetAwaiter().GetResult());
var row = GetFirstRow(await dynamicDataRepository.QueryAsync(selectQuery, connectionString));
ConfigurePage();
ConfigureDataSource(selectQuery, connectionString, dataSourceType);
BuildLayout(listForm, reportFields, row, localizer);
await BuildLayoutAsync(listForm, reportFields, row, localizer, GetImageBaseUrl(configuration));
RequestParameters = false;
}
@ -146,11 +146,12 @@ public class DynamicFormReport : XtraReport
DataMember = QueryName;
}
private void BuildLayout(
private async Task BuildLayoutAsync(
ListForm listForm,
IReadOnlyList<FormReportField> fields,
IDictionary<string, object> row,
IStringLocalizer<PlatformResource> localizer)
IStringLocalizer<PlatformResource> localizer,
string imageBaseUrl)
{
Bands.Clear();
@ -208,7 +209,14 @@ public class DynamicFormReport : XtraReport
var rowHeight = GetFieldRowHeight(field);
var labelWidth = pageWidth * 0.28F;
detail.Controls.Add(CreateCaptionLabel(field, localizer, 0, y, labelWidth, rowHeight));
detail.Controls.Add(CreateValueLabel(field, row, labelWidth, y, pageWidth - labelWidth, rowHeight));
if (field.IsImage)
{
detail.Controls.Add(CreateImageValuePanel(field, row, imageBaseUrl, labelWidth, y, pageWidth - labelWidth, rowHeight));
}
else
{
detail.Controls.Add(CreateValueLabel(field, row, labelWidth, y, pageWidth - labelWidth, rowHeight));
}
y += rowHeight + 3;
}
@ -220,7 +228,7 @@ public class DynamicFormReport : XtraReport
detail.Controls.Add(new XRSubreport
{
BoundsF = new System.Drawing.RectangleF(0, y, pageWidth, 24),
ReportSource = DynamicGridReport.Create(CreateSubReportParameters(subForm.Form, subForm.Filter))
ReportSource = await DynamicGridReport.CreateAsync(CreateSubReportParameters(subForm.Form, subForm.Filter))
});
y += 28;
}
@ -251,7 +259,7 @@ public class DynamicFormReport : XtraReport
};
}
private static XRLabel CreateValueLabel(
private static XRControl CreateValueLabel(
FormReportField field,
IDictionary<string, object> row,
float x,
@ -259,6 +267,11 @@ public class DynamicFormReport : XtraReport
float width,
float height)
{
if (IsHtmlEditor(field))
{
return CreateHtmlValueControl(field, row, x, y, width, height);
}
var label = new XRLabel
{
BoundsF = new System.Drawing.RectangleF(x, y, width, height),
@ -275,6 +288,7 @@ public class DynamicFormReport : XtraReport
if (TryGetFormattedValue(field, row, out var text))
{
label.Text = text;
label.Font = CreateValueFont(text);
}
else
{
@ -284,6 +298,115 @@ public class DynamicFormReport : XtraReport
return label;
}
private static XRRichText CreateHtmlValueControl(
FormReportField field,
IDictionary<string, object> row,
float x,
float y,
float width,
float height)
{
var richText = new XRRichText
{
BoundsF = new System.Drawing.RectangleF(x, y, width, height),
Font = new DXFont("Arial", 8F),
Borders = BorderSide.All,
BorderColor = FieldBorderColor,
BorderWidth = 0.25F,
Padding = new PaddingInfo(5, 5, 2, 2),
CanGrow = true
};
if (TryGetFormattedValue(field, row, out var html))
{
richText.Html = html ?? string.Empty;
}
else
{
richText.ExpressionBindings.Add(new ExpressionBinding("BeforePrint", "Html", $"[{field.BindingName}]"));
}
return richText;
}
private static DXFont CreateValueFont(string text)
{
return ContainsEmoji(text)
? new DXFont("Segoe UI Emoji", 8F)
: new DXFont("Arial", 8F);
}
private static bool ContainsEmoji(string text)
{
if (string.IsNullOrEmpty(text))
{
return false;
}
foreach (var rune in text.EnumerateRunes())
{
if (rune.Value is >= 0x1F000 and <= 0x1FAFF or
>= 0x2600 and <= 0x27BF or
>= 0x2300 and <= 0x23FF)
{
return true;
}
}
return false;
}
private static XRPanel CreateImageValuePanel(
FormReportField field,
IDictionary<string, object> row,
string imageBaseUrl,
float x,
float y,
float width,
float height)
{
var panel = new XRPanel
{
BoundsF = new System.Drawing.RectangleF(x, y, width, height),
Borders = BorderSide.All,
BorderColor = FieldBorderColor,
BorderWidth = 0.25F
};
var rawValue = GetImageRawValue(field, row);
panel.Controls.AddRange(DynamicReportImageHelper.CreateFormPictureBoxes(
rawValue,
imageBaseUrl,
0,
0,
width,
height).ToArray());
return panel;
}
private static object GetImageRawValue(FormReportField field, IDictionary<string, object> row)
{
if (row == null)
{
return null;
}
if (TryGetRowValue(row, field.BindingName, out var value) && HasValue(value))
{
return value;
}
return TryGetRowValue(row, field.Field.FieldName, out value) ? value : null;
}
private static bool HasValue(object value)
{
return value != null &&
value != DBNull.Value &&
!string.IsNullOrWhiteSpace(Convert.ToString(value, CultureInfo.InvariantCulture));
}
private static bool TryGetFormattedValue(
FormReportField field,
IDictionary<string, object> row,
@ -327,6 +450,11 @@ public class DynamicFormReport : XtraReport
private static float GetFieldRowHeight(FormReportField field)
{
if (field.IsImage)
{
return DynamicReportImageHelper.GetFormImageHeight();
}
return field.EditorType is PlatformConsts.EditorTypes.dxTextArea or PlatformConsts.EditorTypes.dxHtmlEditor
? 48
: RowHeight;
@ -489,25 +617,27 @@ public class DynamicFormReport : XtraReport
if (lookup == null || selectField?.JoinOptions != null)
{
return new FormReportField(field, bindingName, editorType, null, null);
return new FormReportField(field, bindingName, editorType, null, null, DynamicReportImageHelper.IsImageEditor(editorType));
}
if (IsReportQueryableLookup(lookup))
if (DynamicReportSqlBuilder.IsReportQueryableLookup(lookup))
{
return new FormReportField(field, GetLookupBindingName(field), editorType, lookup, null);
return new FormReportField(field, DynamicReportSqlBuilder.GetLookupBindingName(field), editorType, lookup, null, false);
}
if (lookup.DataSourceType == UiLookupDataSourceTypeEnum.StaticData)
{
var staticLookupValues = GetStaticLookupValues(lookup, defaultValueHelper);
return new FormReportField(
field,
bindingName,
staticLookupValues?.Count > 0 ? DynamicReportSqlBuilder.GetLookupBindingName(field) : bindingName,
editorType,
lookup,
GetStaticLookupValues(lookup, defaultValueHelper));
staticLookupValues,
false);
}
return new FormReportField(field, bindingName, editorType, null, null);
return new FormReportField(field, bindingName, editorType, null, null, DynamicReportImageHelper.IsImageEditor(editorType));
}
private static string CreateKeyFilter(string keyFieldName, string id)
@ -721,100 +851,6 @@ public class DynamicFormReport : XtraReport
};
}
private static string ApplyLookupQueryColumns(
string selectQuery,
ListForm listForm,
IReadOnlyList<FormReportField> reportFields,
DefaultValueHelper defaultValueHelper)
{
var lookupFields = reportFields
.Where(x => x.Lookup != null && IsReportQueryableLookup(x.Lookup))
.ToList();
if (lookupFields.Count == 0 || string.IsNullOrWhiteSpace(selectQuery))
{
return selectQuery;
}
var tableAlias = string.IsNullOrWhiteSpace(listForm.TableName)
? listForm.SelectCommand
: listForm.TableName;
var selectParts = new List<string>();
var joinParts = new List<string>();
foreach (var field in lookupFields)
{
var lookup = field.Lookup;
var alias = GetLookupAlias(field.Field);
var valueExpr = QuoteSqlIdentifier(lookup.ValueExpr ?? "Key");
var displayExpr = QuoteSqlIdentifier(lookup.DisplayExpr ?? "Name");
var lookupQuery = NormalizeLookupQuery(defaultValueHelper.GetDefaultValue(lookup.LookupQuery));
selectParts.Add($"{alias}.{displayExpr} AS {QuoteSqlIdentifier(field.BindingName)}");
joinParts.Add(
$"LEFT JOIN ({lookupQuery}) AS {alias} " +
$"ON {alias}.{valueExpr} = {QuoteSqlIdentifier(tableAlias)}.{QuoteSqlIdentifier(field.Field.FieldName)}");
}
var fromIndex = selectQuery.IndexOf(" FROM ", StringComparison.OrdinalIgnoreCase);
if (fromIndex < 0)
{
return selectQuery;
}
selectQuery = selectQuery.Insert(fromIndex, "," + string.Join(',', selectParts));
var joinInsertIndex = FindClauseIndex(selectQuery, " WHERE ");
if (joinInsertIndex < 0)
{
joinInsertIndex = FindClauseIndex(selectQuery, " ORDER BY ");
}
if (joinInsertIndex < 0)
{
return selectQuery + " " + string.Join(' ', joinParts);
}
return selectQuery.Insert(joinInsertIndex, " " + string.Join(' ', joinParts));
}
private static bool IsReportQueryableLookup(LookupDto lookup)
{
return lookup.DataSourceType == UiLookupDataSourceTypeEnum.Query &&
string.IsNullOrWhiteSpace(lookup.CascadeParentFields) &&
!string.IsNullOrWhiteSpace(lookup.LookupQuery) &&
!Regex.IsMatch(lookup.LookupQuery, @"@\s*param\d+", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
}
private static int FindClauseIndex(string sql, string clause)
{
return sql.IndexOf(clause, StringComparison.OrdinalIgnoreCase);
}
private static string NormalizeLookupQuery(string lookupQuery)
{
var query = lookupQuery.Trim().TrimEnd(';');
return Regex.Replace(
query,
@"(?<![A-Za-z0-9_])ORDER\s+BY\b[\s\S]*$",
string.Empty,
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
}
private static string GetLookupBindingName(ListFormField field)
{
return $"{field.FieldName}__LookupText";
}
private static string GetLookupAlias(ListFormField field)
{
return QuoteSqlIdentifier($"Lookup_{Regex.Replace(field.FieldName, @"\W+", "_")}");
}
private static string QuoteSqlIdentifier(string identifier)
{
return $"\"{identifier.Replace("\"", "\"\"")}\"";
}
private static string GetLocalizedReportTitle(ListForm listForm, IStringLocalizer<PlatformResource> localizer)
{
var title = string.IsNullOrWhiteSpace(listForm.Title) ? listForm.Name : listForm.Title;
@ -863,6 +899,11 @@ public class DynamicFormReport : XtraReport
field.Field.SourceDbType == DbType.Boolean;
}
private static bool IsHtmlEditor(FormReportField field)
{
return field.EditorType == PlatformConsts.EditorTypes.dxHtmlEditor;
}
private static bool TryFormatBoolean(string value, out string text)
{
if (bool.TryParse(value, out var boolean))
@ -892,6 +933,11 @@ public class DynamicFormReport : XtraReport
return parameters.TryGetValue(name, out var value) ? value : null;
}
private static string GetImageBaseUrl(IConfiguration configuration)
{
return configuration["App:CdnUrl"] ?? configuration["App:SelfUrl"];
}
private sealed record FieldWithEditor(ListFormField Field, string EditorType);
private sealed record FormGroup(string Caption, IReadOnlyList<FormReportField> Fields);
private sealed record FormReportField(
@ -899,5 +945,6 @@ public class DynamicFormReport : XtraReport
string BindingName,
string EditorType,
LookupDto Lookup,
Dictionary<string, string> StaticLookupValues);
Dictionary<string, string> StaticLookupValues,
bool IsImage) : DynamicReportSqlBuilder.IReportLookupColumn;
}

View file

@ -5,13 +5,16 @@ using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using DevExpress.DataAccess.ConnectionParameters;
using DevExpress.DataAccess.Sql;
using DevExpress.Drawing;
using DevExpress.XtraPrinting;
using DevExpress.XtraReports.UI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Sozsoft.Platform;
using Sozsoft.Platform.DynamicData;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Enums;
@ -36,10 +39,10 @@ public class DynamicGridReport : XtraReport
_serviceProvider = serviceProvider;
}
public static DynamicGridReport Create(IDictionary<string, string> parameters)
public static async Task<DynamicGridReport> CreateAsync(IDictionary<string, string> parameters)
{
var report = new DynamicGridReport();
report.Build(parameters);
await report.BuildAsync(parameters);
return report;
}
@ -47,7 +50,7 @@ public class DynamicGridReport : XtraReport
{
}
private void Build(IDictionary<string, string> parameters)
private async Task BuildAsync(IDictionary<string, string> parameters)
{
if (_serviceProvider == null)
{
@ -68,20 +71,21 @@ public class DynamicGridReport : XtraReport
var selectQueryManager = scope.ServiceProvider.GetRequiredService<ISelectQueryManager>();
var defaultValueHelper = scope.ServiceProvider.GetRequiredService<DefaultValueHelper>();
var localizer = scope.ServiceProvider.GetRequiredService<IStringLocalizer<PlatformResource>>();
var configuration = scope.ServiceProvider.GetRequiredService<IConfiguration>();
if (!authManager.CanAccess(listFormCode).GetAwaiter().GetResult())
if (!await authManager.CanAccess(listFormCode))
{
throw new UnauthorizedAccessException($"No read permission for list form '{listFormCode}'.");
}
var listForm = listFormManager.GetUserListForm(listFormCode).GetAwaiter().GetResult();
var fields = listFormFieldManager.GetUserListFormFields(listFormCode).GetAwaiter().GetResult()
var listForm = await listFormManager.GetUserListForm(listFormCode);
var fields = (await listFormFieldManager.GetUserListFormFields(listFormCode))
.Where(x => x.CanRead)
.OrderBy(x => x.ListOrderNo ?? int.MaxValue)
.ThenBy(x => x.CaptionName)
.ToList();
var customizations = customizationManager.GetUsersServerCustomizations(listFormCode).GetAwaiter().GetResult();
var (_, connectionString, dataSourceType) = dynamicDataManager.GetAsync(listForm.IsTenant, listForm.DataSourceCode).GetAwaiter().GetResult();
var customizations = await customizationManager.GetUsersServerCustomizations(listFormCode);
var (dynamicDataRepository, connectionString, dataSourceType) = await dynamicDataManager.GetAsync(listForm.IsTenant, listForm.DataSourceCode);
var queryParams = new QueryParameters
{
@ -117,24 +121,31 @@ public class DynamicGridReport : XtraReport
.Select(field =>
{
var selectField = selectQueryManager.SelectFields.First(s => s.FieldName == field.FieldName);
return CreateReportColumn(field, selectField, defaultValueHelper);
return CreateReportColumn(
field,
selectField,
defaultValueHelper,
DynamicReportImageHelper.HasImageEditor(listForm, field.FieldName),
HasHtmlEditor(listForm, field.FieldName));
})
.Where(x => !string.IsNullOrWhiteSpace(x.BindingName) && x.BindingName != "undefined")
.ToList();
var selectQuery = ApplyLookupQueryColumns(
selectQueryManager.SelectQuery,
listForm,
var selectQuery = StripReportPaging(ApplyQueryParameters(selectQueryManager.SelectQuery, selectQueryManager.SelectQueryParameters));
selectQuery = DynamicReportSqlBuilder.ApplyLookupTextColumns(
selectQuery,
reportColumns,
defaultValueHelper);
selectQuery = StripReportPaging(ApplyQueryParameters(selectQuery, selectQueryManager.SelectQueryParameters));
defaultValueHelper,
dataSourceType);
var imageBaseUrl = GetImageBaseUrl(configuration);
reportColumns = await ApplyImageCountsAsync(reportColumns, dynamicDataRepository, selectQuery, connectionString, imageBaseUrl);
ConfigurePage(reportColumns);
ConfigureDataSource(
selectQuery,
connectionString,
dataSourceType);
BuildLayout(listForm, reportColumns, localizer);
BuildLayout(listForm, reportColumns, localizer, imageBaseUrl);
RequestParameters = false;
}
@ -144,7 +155,7 @@ public class DynamicGridReport : XtraReport
Margins = CreateReportMargins(HorizontalMargin, HorizontalMargin, 35, 35);
PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4;
var portraitContentWidth = A4PortraitWidth - Margins.Left - Margins.Right;
var requestedContentWidth = visibleFields.Sum(x => Math.Max(x.Field.Width ?? 90, 60));
var requestedContentWidth = visibleFields.Sum(x => x.IsImage ? GetImageColumnWidth(x) : GetPreferredColumnWidth(x.Field));
Landscape = visibleFields.Count >= LandscapeColumnThreshold || requestedContentWidth > portraitContentWidth;
Font = new DXFont("Arial", 8.25F);
}
@ -242,10 +253,80 @@ public class DynamicGridReport : XtraReport
};
}
private static async Task<List<ReportColumn>> ApplyImageCountsAsync(
IReadOnlyList<ReportColumn> columns,
IDynamicDataRepository dynamicDataRepository,
string selectQuery,
string connectionString,
string imageBaseUrl)
{
var imageColumns = columns.Where(x => x.IsImage).ToList();
if (imageColumns.Count == 0)
{
return columns.ToList();
}
var imageCounts = imageColumns.ToDictionary(x => x.BindingName, _ => 1, StringComparer.OrdinalIgnoreCase);
var rows = await dynamicDataRepository.QueryAsync(selectQuery, connectionString);
foreach (var row in rows.OfType<IDictionary<string, object>>())
{
foreach (var column in imageColumns)
{
var count = DynamicReportImageHelper.CountImageSources(
GetImageRawValue(row, column),
imageBaseUrl);
if (count > imageCounts[column.BindingName])
{
imageCounts[column.BindingName] = count;
}
}
}
return columns
.Select(column => column.IsImage
? column with { ImageCount = imageCounts[column.BindingName] }
: column)
.ToList();
}
private static object GetImageRawValue(IDictionary<string, object> row, ReportColumn column)
{
if (TryGetRowValue(row, column.BindingName, out var value) && HasValue(value))
{
return value;
}
return TryGetRowValue(row, column.Field.FieldName, out value) ? value : null;
}
private static bool HasValue(object value)
{
return value != null &&
value != DBNull.Value &&
!string.IsNullOrWhiteSpace(Convert.ToString(value, CultureInfo.InvariantCulture));
}
private static bool TryGetRowValue(IDictionary<string, object> row, string name, out object value)
{
foreach (var item in row)
{
if (string.Equals(item.Key, name, StringComparison.OrdinalIgnoreCase))
{
value = item.Value;
return true;
}
}
value = null;
return false;
}
private void BuildLayout(
ListForm listForm,
IReadOnlyList<ReportColumn> fields,
IStringLocalizer<PlatformResource> localizer)
IStringLocalizer<PlatformResource> localizer,
string imageBaseUrl)
{
Bands.Clear();
@ -253,7 +334,12 @@ public class DynamicGridReport : XtraReport
var bottomMargin = new BottomMarginBand { HeightF = 32 };
var reportHeader = new ReportHeaderBand { HeightF = 32 };
var pageHeader = new PageHeaderBand { HeightF = 28 };
var detail = new DetailBand { HeightF = 24 };
var detailHeight = fields.Any(x => x.IsImage)
? DynamicReportImageHelper.GetGridImageHeight()
: fields.Any(x => x.IsHtml)
? 48
: 24;
var detail = new DetailBand { HeightF = detailHeight };
var reportFooter = new ReportFooterBand { HeightF = HasAnySummary(fields) ? 28 : 0 };
Bands.AddRange([topMargin, reportHeader, pageHeader, detail, reportFooter, bottomMargin]);
@ -288,17 +374,17 @@ public class DynamicGridReport : XtraReport
var widths = CalculateWidths(fields, pageWidth);
var headerTable = CreateTable(pageWidth, 28);
var headerRow = new XRTableRow();
var detailTable = CreateTable(pageWidth, 24);
var detailRow = new XRTableRow();
var headerRow = new XRTableRow { HeightF = 28 };
var detailTable = CreateTable(pageWidth, detailHeight);
var detailRow = new XRTableRow { HeightF = detailHeight };
var footerTable = CreateTable(pageWidth, 24);
var footerRow = new XRTableRow();
var footerRow = new XRTableRow { HeightF = 24 };
for (var i = 0; i < fields.Count; i++)
{
var field = fields[i];
headerRow.Cells.Add(CreateHeaderCell(field, widths[i], localizer));
detailRow.Cells.Add(CreateDetailCell(field, widths[i]));
detailRow.Cells.Add(CreateDetailCell(field, widths[i], imageBaseUrl, detailHeight));
footerRow.Cells.Add(CreateFooterCell(field, widths[i]));
}
@ -335,6 +421,7 @@ public class DynamicGridReport : XtraReport
{
Text = GetLocalizedFieldCaption(field, localizer),
WidthF = width,
Weight = width,
Font = new DXFont("Arial", 8F, DXFontStyle.Bold),
BackColor = System.Drawing.Color.FromArgb(238, 242, 247),
Padding = new PaddingInfo(4, 4, 2, 2),
@ -368,13 +455,29 @@ public class DynamicGridReport : XtraReport
: localized.Value;
}
private static XRTableCell CreateDetailCell(ReportColumn column, float width)
private static XRTableCell CreateDetailCell(ReportColumn column, float width, string imageBaseUrl, float height)
{
var field = column.Field;
if (column.IsImage)
{
return DynamicReportImageHelper.CreateGridImageCell(
column.BindingName,
column.Field.FieldName,
width,
imageBaseUrl,
column.ImageCount);
}
if (column.IsHtml)
{
return CreateHtmlDetailCell(column, width, height);
}
var cell = new XRTableCell
{
WidthF = width,
Font = new DXFont("Arial", 8F),
Weight = width,
Font = new DXFont("Segoe UI Emoji", 8F),
Padding = new PaddingInfo(4, 4, 2, 2),
TextAlignment = ToTextAlignment(field.Alignment, field.SourceDbType),
Multiline = false,
@ -385,6 +488,29 @@ public class DynamicGridReport : XtraReport
return cell;
}
private static XRTableCell CreateHtmlDetailCell(ReportColumn column, float width, float height)
{
var cell = new XRTableCell
{
WidthF = width,
Weight = width,
Padding = new PaddingInfo(0, 0, 0, 0),
CanGrow = false
};
var richText = new XRRichText
{
BoundsF = new System.Drawing.RectangleF(0, 0, width, height),
Font = new DXFont("Arial", 8F),
Padding = new PaddingInfo(4, 4, 2, 2),
CanGrow = false
};
richText.ExpressionBindings.Add(new ExpressionBinding("BeforePrint", "Html", $"[{column.BindingName}]"));
cell.Controls.Add(richText);
return cell;
}
private static XRTableCell CreateFooterCell(ReportColumn column, float width)
{
var field = column.Field;
@ -392,6 +518,7 @@ public class DynamicGridReport : XtraReport
var cell = new XRTableCell
{
WidthF = width,
Weight = width,
Font = new DXFont("Arial", 8F, DXFontStyle.Bold),
Padding = new PaddingInfo(4, 4, 2, 2),
TextAlignment = ToTextAlignment(field.Alignment, field.SourceDbType)
@ -427,7 +554,7 @@ public class DynamicGridReport : XtraReport
private static float[] CalculateWidths(IReadOnlyList<ReportColumn> fields, float pageWidth)
{
var raw = fields.Select(x => (float)Math.Max(x.Field.Width ?? 90, 60)).ToArray();
var raw = fields.Select(x => x.IsImage ? GetImageColumnWidth(x) : GetPreferredColumnWidth(x.Field)).ToArray();
var total = raw.Sum();
if (total <= 0)
{
@ -437,11 +564,64 @@ public class DynamicGridReport : XtraReport
return raw.Select(x => pageWidth * x / total).ToArray();
}
private static float GetPreferredColumnWidth(ListFormField field)
{
if (field.Width > 0)
{
return Math.Max(field.Width.Value, 60);
}
var caption = string.IsNullOrWhiteSpace(field.CaptionName) ? field.FieldName : field.CaptionName;
var name = field.FieldName ?? string.Empty;
var text = $"{caption} {name}".ToLowerInvariant();
if (field.SourceDbType == DbType.Boolean)
{
return 75;
}
if (IsNumeric(field.SourceDbType))
{
return 70;
}
if (field.SourceDbType is DbType.Date or DbType.DateTime or DbType.DateTime2 or DbType.DateTimeOffset)
{
return 115;
}
if (text.Contains("description") ||
text.Contains("explanation") ||
text.Contains("note") ||
text.Contains("comment") ||
text.Contains("address") ||
text.Contains("content") ||
text.Contains("html") ||
text.Contains("text"))
{
return 220;
}
if (text.Contains("name") ||
text.Contains("title") ||
text.Contains("subject"))
{
return 160;
}
return Math.Clamp((caption?.Length ?? 0) * 9F + 36F, 85F, 180F);
}
private static bool HasAnySummary(IEnumerable<ReportColumn> fields)
{
return fields.Any(x => GetSummaryType(x.Field.TotalSummaryJson) != null);
}
private static float GetImageColumnWidth(ReportColumn column)
{
return DynamicReportImageHelper.GetGridImageColumnWidth(column.ImageCount);
}
private static string GetReportFieldName(SelectField selectField)
{
if (selectField?.JoinOptions != null && !string.IsNullOrWhiteSpace(selectField.JoinOptions.FieldNameAlias))
@ -455,19 +635,21 @@ public class DynamicGridReport : XtraReport
private static ReportColumn CreateReportColumn(
ListFormField field,
SelectField selectField,
DefaultValueHelper defaultValueHelper)
DefaultValueHelper defaultValueHelper,
bool isImage,
bool isHtml)
{
var bindingName = GetReportFieldName(selectField);
var lookup = GetLookup(field);
if (lookup == null || selectField?.JoinOptions != null)
{
return new ReportColumn(field, bindingName, null, null);
return new ReportColumn(field, bindingName, null, null, isImage, isHtml);
}
if (IsReportQueryableLookup(lookup))
if (DynamicReportSqlBuilder.IsReportQueryableLookup(lookup))
{
return new ReportColumn(field, GetLookupBindingName(field), lookup, null);
return new ReportColumn(field, DynamicReportSqlBuilder.GetLookupBindingName(field), lookup, null, isImage, isHtml);
}
if (lookup.DataSourceType == UiLookupDataSourceTypeEnum.StaticData)
@ -475,20 +657,35 @@ public class DynamicGridReport : XtraReport
var staticLookupValues = GetStaticLookupValues(lookup, defaultValueHelper);
return new ReportColumn(
field,
staticLookupValues?.Count > 0 ? GetLookupBindingName(field) : bindingName,
staticLookupValues?.Count > 0 ? DynamicReportSqlBuilder.GetLookupBindingName(field) : bindingName,
lookup,
staticLookupValues);
staticLookupValues,
isImage,
isHtml);
}
return new ReportColumn(field, bindingName, null, null);
return new ReportColumn(field, bindingName, null, null, isImage, isHtml);
}
private static bool IsReportQueryableLookup(LookupDto lookup)
private static bool HasHtmlEditor(ListForm listForm, string fieldName)
{
return lookup.DataSourceType == UiLookupDataSourceTypeEnum.Query &&
string.IsNullOrWhiteSpace(lookup.CascadeParentFields) &&
!string.IsNullOrWhiteSpace(lookup.LookupQuery) &&
!Regex.IsMatch(lookup.LookupQuery, @"@\s*param\d+", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
if (string.IsNullOrWhiteSpace(listForm?.EditingFormJson) || string.IsNullOrWhiteSpace(fieldName))
{
return false;
}
try
{
var groups = JsonSerializer.Deserialize<List<EditingFormDto>>(listForm.EditingFormJson) ?? [];
return groups
.SelectMany(x => x.Items ?? [])
.Any(x => string.Equals(x.DataField, fieldName, StringComparison.OrdinalIgnoreCase) &&
x.EditorType2 == PlatformConsts.EditorTypes.dxHtmlEditor);
}
catch (JsonException)
{
return false;
}
}
private static LookupDto GetLookup(ListFormField field)
@ -582,119 +779,15 @@ public class DynamicGridReport : XtraReport
};
}
private static string ApplyLookupQueryColumns(
string selectQuery,
ListForm listForm,
IReadOnlyList<ReportColumn> reportColumns,
DefaultValueHelper defaultValueHelper)
{
var queryLookupColumns = reportColumns
.Where(x => x.Lookup != null && IsReportQueryableLookup(x.Lookup))
.ToList();
var staticLookupColumns = reportColumns
.Where(x => x.StaticLookupValues?.Count > 0)
.ToList();
if ((queryLookupColumns.Count == 0 && staticLookupColumns.Count == 0) ||
string.IsNullOrWhiteSpace(selectQuery))
{
return selectQuery;
}
var tableAlias = string.IsNullOrWhiteSpace(listForm.TableName)
? listForm.SelectCommand
: listForm.TableName;
var selectParts = new List<string>();
var joinParts = new List<string>();
foreach (var column in staticLookupColumns)
{
selectParts.Add(CreateStaticLookupSelectPart(column, tableAlias));
}
foreach (var column in queryLookupColumns)
{
var lookup = column.Lookup;
var alias = GetLookupAlias(column.Field);
var valueExpr = QuoteSqlIdentifier(lookup.ValueExpr ?? "Key");
var displayExpr = QuoteSqlIdentifier(lookup.DisplayExpr ?? "Name");
var lookupQuery = NormalizeLookupQuery(defaultValueHelper.GetDefaultValue(lookup.LookupQuery));
selectParts.Add($"{alias}.{displayExpr} AS {QuoteSqlIdentifier(column.BindingName)}");
joinParts.Add(
$"LEFT JOIN ({lookupQuery}) AS {alias} " +
$"ON {alias}.{valueExpr} = {QuoteSqlIdentifier(tableAlias)}.{QuoteSqlIdentifier(column.Field.FieldName)}");
}
var fromIndex = selectQuery.IndexOf(" FROM ", StringComparison.OrdinalIgnoreCase);
if (fromIndex < 0)
{
return selectQuery;
}
selectQuery = selectQuery.Insert(fromIndex, "," + string.Join(',', selectParts));
var joinInsertIndex = FindClauseIndex(selectQuery, " WHERE ");
if (joinInsertIndex < 0)
{
joinInsertIndex = FindClauseIndex(selectQuery, " ORDER BY ");
}
if (joinParts.Count == 0)
{
return selectQuery;
}
if (joinInsertIndex < 0)
{
return selectQuery + " " + string.Join(' ', joinParts);
}
return selectQuery.Insert(joinInsertIndex, " " + string.Join(' ', joinParts));
}
private static string CreateStaticLookupSelectPart(ReportColumn column, string tableAlias)
{
var sourceField = $"{QuoteSqlIdentifier(tableAlias)}.{QuoteSqlIdentifier(column.Field.FieldName)}";
var cases = column.StaticLookupValues
.Select(item => $"WHEN {ToSqlLiteral(item.Key)} THEN {ToSqlLiteral(item.Value)}");
return $"CASE {sourceField} {string.Join(" ", cases)} ELSE CAST({sourceField} AS VARCHAR(4000)) END AS {QuoteSqlIdentifier(column.BindingName)}";
}
private static int FindClauseIndex(string sql, string clause)
{
return sql.IndexOf(clause, StringComparison.OrdinalIgnoreCase);
}
private static string NormalizeLookupQuery(string lookupQuery)
{
var query = lookupQuery.Trim().TrimEnd(';');
return Regex.Replace(
query,
@"(?<![A-Za-z0-9_])ORDER\s+BY\b[\s\S]*$",
string.Empty,
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
}
private static string GetLookupBindingName(ListFormField field)
{
return $"{field.FieldName}__LookupText";
}
private static string GetLookupAlias(ListFormField field)
{
return QuoteSqlIdentifier($"Lookup_{Regex.Replace(field.FieldName, @"\W+", "_")}");
}
private static string QuoteSqlIdentifier(string identifier)
{
return $"\"{identifier.Replace("\"", "\"\"")}\"";
}
private sealed record ReportColumn(
ListFormField Field,
string BindingName,
LookupDto Lookup,
Dictionary<string, string> StaticLookupValues);
Dictionary<string, string> StaticLookupValues,
bool IsImage,
bool IsHtml,
int ImageCount = 1) : DynamicReportSqlBuilder.IReportLookupColumn;
private static SummaryFunc? GetSummaryType(string totalSummaryJson)
{
@ -749,4 +842,9 @@ public class DynamicGridReport : XtraReport
{
return parameters.TryGetValue(name, out var value) ? value : null;
}
private static string GetImageBaseUrl(IConfiguration configuration)
{
return configuration["App:CdnUrl"] ?? configuration["App:SelfUrl"];
}
}

View file

@ -0,0 +1,754 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using DevExpress.Data.Filtering;
using DevExpress.XtraPrinting;
using DevExpress.XtraPrinting.Drawing;
using DevExpress.XtraReports.UI;
using Sozsoft.Platform;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.ListForms;
namespace Sozsoft.Reports.PredefinedReports;
internal static class DynamicReportImageHelper
{
private const float DefaultGridImageHeight = 82F;
private const float DefaultGridImageWidth = 86F;
private const float DefaultFormImageHeight = 86F;
private const float MaxFormImageWidth = 86F;
private const int MaxFormImages = 8;
private const string ImageUrlFunctionName = "SozReportImageUrl";
private const string ImageSourceFunctionName = "SozReportImageSource";
private static readonly HttpClient HttpClient = new(new HttpClientHandler
{
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
})
{
Timeout = TimeSpan.FromSeconds(5)
};
static DynamicReportImageHelper()
{
RegisterImageFunction(new ReportImageUrlFunction());
RegisterImageFunction(new ReportImageSourceFunction());
}
public static bool IsImageEditor(string editorType)
{
return editorType is PlatformConsts.EditorTypes.dxImageUpload or PlatformConsts.EditorTypes.dxImageViewer;
}
public static bool HasImageEditor(ListForm listForm, string fieldName)
{
if (string.IsNullOrWhiteSpace(listForm?.EditingFormJson) || string.IsNullOrWhiteSpace(fieldName))
{
return false;
}
try
{
var groups = JsonSerializer.Deserialize<List<EditingFormDto>>(listForm.EditingFormJson) ?? [];
return groups
.SelectMany(x => x.Items ?? [])
.Any(x => string.Equals(x.DataField, fieldName, StringComparison.OrdinalIgnoreCase) &&
IsImageEditor(x.EditorType2));
}
catch (JsonException)
{
return false;
}
}
public static float GetGridImageHeight() => DefaultGridImageHeight;
public static float GetFormImageHeight() => DefaultFormImageHeight;
public static XRTableCell CreateGridImageCell(
string bindingName,
string fieldName,
float width,
string baseUrl,
int imageCount)
{
var safeImageCount = Math.Max(imageCount, 1);
var cell = new XRTableCell
{
WidthF = width,
Weight = width,
Padding = new PaddingInfo(3, 3, 3, 3),
CanGrow = false
};
for (var i = 0; i < safeImageCount; i++)
{
var pictureBox = CreatePictureBox(i, width, DefaultGridImageHeight - 6, safeImageCount);
pictureBox.Visible = true;
pictureBox.ExpressionBindings.Add(new ExpressionBinding(
"BeforePrint",
"ImageUrl",
$"{ImageUrlFunctionName}([{bindingName}], '{EscapeExpressionString(baseUrl)}', {i})"));
pictureBox.ExpressionBindings.Add(new ExpressionBinding(
"BeforePrint",
"ImageSource",
$"{ImageSourceFunctionName}([{bindingName}], '{EscapeExpressionString(baseUrl)}', {i})"));
cell.Controls.Add(pictureBox);
}
return cell;
}
public static float GetGridImageColumnWidth(int imageCount)
{
var safeImageCount = Math.Max(imageCount, 1);
return safeImageCount * DefaultGridImageWidth + (safeImageCount - 1) * 4F + 8F;
}
private static object GetCurrentValue(XRControl control, string bindingName, string fieldName)
{
if (HasImageValue(control.Tag))
{
return control.Tag;
}
var report = control.Report;
if (report == null)
{
return null;
}
var value = GetCurrentColumnValue(report, bindingName);
return HasImageValue(value) || string.Equals(bindingName, fieldName, StringComparison.OrdinalIgnoreCase)
? value
: GetCurrentColumnValue(report, fieldName);
}
private static object GetCurrentColumnValue(XtraReportBase report, string fieldName)
{
if (string.IsNullOrWhiteSpace(fieldName))
{
return null;
}
try
{
return report.GetCurrentColumnValue(fieldName);
}
catch (InvalidOperationException)
{
return null;
}
catch (ArgumentException)
{
return null;
}
}
private static bool HasValue(object value)
{
return value != null &&
value != DBNull.Value &&
!string.IsNullOrWhiteSpace(Convert.ToString(value));
}
private static bool HasImageValue(object value)
{
return HasValue(value) && NormalizeImageValue(value).Any(IsPotentialImageSource);
}
private static bool IsPotentialImageSource(string value)
{
if (string.IsNullOrWhiteSpace(value) ||
value.StartsWith("System.", StringComparison.Ordinal))
{
return false;
}
return value.StartsWith("data:image/", StringComparison.OrdinalIgnoreCase) ||
value.StartsWith("/", StringComparison.Ordinal) ||
IsProbablyBase64Image(value) ||
Uri.TryCreate(value, UriKind.Absolute, out _);
}
public static IReadOnlyList<XRPictureBox> CreateFormPictureBoxes(
object rawValue,
string baseUrl,
float x,
float y,
float width,
float height)
{
var innerWidth = Math.Max(width - 10, 1);
var innerHeight = Math.Max(height - 10, 1);
var imageSources = ResolveImageSources(rawValue, baseUrl, MaxFormImages).ToList();
var pictureBoxes = CreateFormPictureBoxes(innerWidth, innerHeight, Math.Max(imageSources.Count, 1))
.Select(picture =>
{
picture.LeftF += x + 5;
picture.TopF += y + 5;
return picture;
})
.ToList();
ApplyImages(pictureBoxes, imageSources);
return pictureBoxes;
}
public static int CountImageSources(object rawValue, string baseUrl)
{
return ResolveImageSources(rawValue, baseUrl, int.MaxValue).Count();
}
private static XRPictureBox CreatePictureBox(int index, float width, float height, int count)
{
var gap = 4F;
var safeCount = Math.Max(count, 1);
var boxWidth = safeCount == 1
? width
: Math.Max((width - gap * (safeCount - 1)) / safeCount, 1);
return CreatePictureBox(index * (boxWidth + gap), 0, boxWidth, height);
}
private static XRPictureBox CreatePictureBox(float x, float y, float width, float height)
{
return new XRPictureBox
{
BoundsF = new System.Drawing.RectangleF(x, y, width, height),
Sizing = ImageSizeMode.ZoomImage,
Visible = false
};
}
private static IEnumerable<XRPictureBox> CreateFormPictureBoxes(float width, float height, int count)
{
var safeCount = Math.Max(count, 1);
var gap = 8F;
var boxWidth = Math.Min(MaxFormImageWidth, Math.Max((width - gap * (safeCount - 1)) / safeCount, 1));
for (var i = 0; i < safeCount; i++)
{
yield return CreatePictureBox(i * (boxWidth + gap), 0, boxWidth, height);
}
}
private static void ArrangePictureBoxes(
IReadOnlyList<XRPictureBox> pictureBoxes,
float width,
float height,
int visibleCount)
{
var safeCount = Math.Max(visibleCount, 1);
var gap = 4F;
var boxWidth = safeCount == 1
? width
: Math.Max((width - gap * (safeCount - 1)) / safeCount, 1);
for (var i = 0; i < pictureBoxes.Count; i++)
{
pictureBoxes[i].BoundsF = new System.Drawing.RectangleF(
i * (boxWidth + gap),
0,
boxWidth,
height);
}
}
private static string GetImageSourceAt(object rawValue, string baseUrl, int index)
{
if (index < 0)
{
return null;
}
return ResolveImageSources(rawValue, baseUrl, index + 1).ElementAtOrDefault(index);
}
private static string GetImageUrlAt(object rawValue, string baseUrl, int index)
{
var imageSource = GetImageSourceAt(rawValue, baseUrl, index);
return Uri.TryCreate(imageSource, UriKind.Absolute, out var uri) &&
uri.Scheme is "http" or "https"
? imageSource
: null;
}
private static ImageSource GetImageSourceObjectAt(object rawValue, string baseUrl, int index)
{
var imageSource = GetImageSourceAt(rawValue, baseUrl, index);
if (string.IsNullOrWhiteSpace(imageSource) ||
!TryGetImageBytes(imageSource, out var bytes))
{
return null;
}
try
{
return new ImageSource(false, bytes);
}
catch (ArgumentException)
{
return null;
}
catch (InvalidOperationException)
{
return null;
}
}
private static void ApplyImages(IReadOnlyList<XRPictureBox> pictureBoxes, IReadOnlyList<string> imageSources)
{
for (var i = 0; i < pictureBoxes.Count; i++)
{
ApplyImage(pictureBoxes[i], i < imageSources.Count ? imageSources[i] : null);
}
}
private static void ApplyImage(XRPictureBox picture, string imageSource)
{
picture.ImageSource = null;
picture.ImageUrl = null;
picture.Visible = false;
if (string.IsNullOrWhiteSpace(imageSource))
{
return;
}
if (Uri.TryCreate(imageSource, UriKind.Absolute, out var uri) &&
uri.Scheme is "http" or "https")
{
picture.ImageUrl = imageSource;
picture.Visible = true;
return;
}
if (TryGetImageBytes(imageSource, out var bytes))
{
try
{
picture.ImageSource = new ImageSource(false, bytes);
picture.Visible = true;
return;
}
catch (ArgumentException)
{
picture.ImageSource = null;
}
catch (InvalidOperationException)
{
picture.ImageSource = null;
}
}
}
private static string ResolveImageSource(string imageSource, string baseUrl)
{
if (string.IsNullOrWhiteSpace(imageSource) ||
imageSource.StartsWith("data:image/", StringComparison.OrdinalIgnoreCase) ||
Uri.TryCreate(imageSource, UriKind.Absolute, out _))
{
return imageSource;
}
if (string.IsNullOrWhiteSpace(baseUrl))
{
return imageSource;
}
return $"{baseUrl.TrimEnd('/')}/{imageSource.TrimStart('/')}";
}
private static IEnumerable<string> ResolveImageSources(object rawValue, string baseUrl, int take)
{
return NormalizeImageValue(rawValue)
.Select(x => ResolveImageSource(x, baseUrl))
.Where(IsPotentialImageSource)
.Take(take);
}
private static bool TryGetImageBytes(string imageSource, out byte[] bytes)
{
bytes = null;
if (TryGetDataUrlBytes(imageSource, out bytes))
{
return true;
}
if (IsProbablyBase64Image(imageSource))
{
try
{
bytes = Convert.FromBase64String(imageSource);
return bytes.Length > 0;
}
catch (FormatException)
{
bytes = null;
return false;
}
}
if (!Uri.TryCreate(imageSource, UriKind.Absolute, out var uri) ||
uri.Scheme is not ("http" or "https"))
{
return false;
}
try
{
bytes = HttpClient.GetByteArrayAsync(uri).GetAwaiter().GetResult();
return bytes?.Length > 0;
}
catch (HttpRequestException)
{
bytes = null;
return false;
}
catch (TaskCanceledException)
{
bytes = null;
return false;
}
catch (InvalidOperationException)
{
bytes = null;
return false;
}
}
private static bool TryGetDataUrlBytes(string imageSource, out byte[] bytes)
{
bytes = null;
var match = Regex.Match(
imageSource,
@"^data:image/[^;]+;base64,(?<data>.+)$",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
if (!match.Success)
{
return false;
}
try
{
bytes = Convert.FromBase64String(match.Groups["data"].Value);
return bytes.Length > 0;
}
catch (FormatException)
{
bytes = null;
return false;
}
}
private static IEnumerable<string> NormalizeImageValue(object value)
{
if (value == null || value == DBNull.Value)
{
yield break;
}
if (value is JsonElement element)
{
foreach (var source in NormalizeJsonElement(element))
{
yield return source;
}
yield break;
}
if (value is IEnumerable enumerable && value is not string)
{
foreach (var item in enumerable)
{
foreach (var source in NormalizeImageValue(item))
{
yield return source;
}
}
yield break;
}
var text = Convert.ToString(value)?.Trim();
if (string.IsNullOrWhiteSpace(text))
{
yield break;
}
if (text.StartsWith("[", StringComparison.Ordinal))
{
foreach (var source in NormalizeJsonArray(text))
{
yield return source;
}
yield break;
}
foreach (var source in SplitImageString(text))
{
yield return source;
}
}
private static IEnumerable<string> NormalizeJsonArray(string value)
{
JsonDocument document = null;
string fallbackSource = null;
try
{
document = JsonDocument.Parse(value);
}
catch (JsonException)
{
fallbackSource = ToImageSource(value);
}
if (document == null)
{
if (!string.IsNullOrWhiteSpace(fallbackSource))
{
yield return fallbackSource;
}
yield break;
}
using (document)
{
if (document.RootElement.ValueKind != JsonValueKind.Array)
{
yield break;
}
foreach (var item in document.RootElement.EnumerateArray())
{
foreach (var source in NormalizeJsonElement(item))
{
yield return source;
}
}
}
}
private static IEnumerable<string> NormalizeJsonElement(JsonElement element)
{
if (element.ValueKind == JsonValueKind.String)
{
foreach (var source in SplitImageString(element.GetString()))
{
yield return source;
}
yield break;
}
if (element.ValueKind != JsonValueKind.Object)
{
yield break;
}
foreach (var propertyName in new[] { "url", "imageUrl", "src", "fileUrl", "fileName", "path", "value" })
{
if (TryGetJsonProperty(element, propertyName, out var property) &&
property.ValueKind == JsonValueKind.String)
{
foreach (var source in SplitImageString(property.GetString()))
{
yield return source;
}
yield break;
}
}
}
private static bool TryGetJsonProperty(JsonElement element, string propertyName, out JsonElement value)
{
foreach (var property in element.EnumerateObject())
{
if (string.Equals(property.Name, propertyName, StringComparison.OrdinalIgnoreCase))
{
value = property.Value;
return true;
}
}
value = default;
return false;
}
private static IEnumerable<string> SplitImageString(string value)
{
var text = value?.Trim();
if (string.IsNullOrWhiteSpace(text))
{
yield break;
}
if (text.StartsWith("data:image/", StringComparison.OrdinalIgnoreCase))
{
yield return text;
yield break;
}
foreach (var item in SplitDelimitedImageString(text)
.Select(x => ToImageSource(x))
.Where(x => !string.IsNullOrWhiteSpace(x)))
{
yield return item;
}
}
private static IEnumerable<string> SplitDelimitedImageString(string text)
{
var start = 0;
for (var i = 0; i < text.Length; i++)
{
if (!IsImageDelimiter(text, i))
{
continue;
}
yield return text[start..i];
start = text[i] == '\r' && i + 1 < text.Length && text[i + 1] == '\n'
? i + 2
: i + 1;
if (start == i + 2)
{
i++;
}
}
yield return text[start..];
}
private static bool IsImageDelimiter(string text, int index)
{
return text[index] == '|' ||
text[index] == '\n' ||
text[index] == '\r' ||
(text[index] == ',' && !IsDataImageComma(text, index));
}
private static bool IsDataImageComma(string text, int index)
{
var dataImageIndex = text.LastIndexOf("data:image/", index, StringComparison.OrdinalIgnoreCase);
if (dataImageIndex < 0)
{
return false;
}
var previousDelimiterIndex = text.LastIndexOfAny(['|', '\r', '\n'], index);
return previousDelimiterIndex < dataImageIndex &&
text.IndexOf(";base64,", dataImageIndex, index - dataImageIndex + 1, StringComparison.OrdinalIgnoreCase) >= 0;
}
private static string EscapeExpressionString(string value)
{
return (value ?? string.Empty).Replace("'", "''");
}
private static string ToImageSource(string value)
{
var text = value?.Trim();
if (string.IsNullOrWhiteSpace(text))
{
return null;
}
if (text.StartsWith("data:image/", StringComparison.OrdinalIgnoreCase) || !IsProbablyBase64Image(text))
{
return text;
}
return $"data:image/jpeg;base64,{text}";
}
private static bool IsProbablyBase64Image(string value)
{
return value.Length > 80 && Regex.IsMatch(value, @"^[A-Za-z0-9+/]+={0,2}$", RegexOptions.CultureInvariant);
}
private static void RegisterImageFunction(ICustomFunctionOperator function)
{
if (CriteriaOperator.GetCustomFunction(function.Name) == null)
{
CriteriaOperator.RegisterCustomFunction(function);
}
}
private abstract class ReportImageFunctionBase : ICustomFunctionOperatorBrowsable
{
public int MinOperandCount => 3;
public int MaxOperandCount => 3;
public FunctionCategory Category => FunctionCategory.Text;
public string Description => "Returns a report image value.";
public abstract string Name { get; }
protected abstract Type FunctionResultType { get; }
protected abstract object GetValue(object rawValue, string baseUrl, int index);
public object Evaluate(params object[] operands)
{
return GetValue(
operands.ElementAtOrDefault(0),
Convert.ToString(operands.ElementAtOrDefault(1), CultureInfo.InvariantCulture),
ToIndex(operands.ElementAtOrDefault(2)));
}
public Type ResultType(params Type[] operands)
{
return FunctionResultType;
}
public bool IsValidOperandCount(int count)
{
return count == 3;
}
public bool IsValidOperandType(int operandIndex, int operandCount, Type type)
{
return true;
}
private static int ToIndex(object value)
{
return value == null || value == DBNull.Value
? -1
: Convert.ToInt32(value, CultureInfo.InvariantCulture);
}
}
private sealed class ReportImageUrlFunction : ReportImageFunctionBase
{
public override string Name => ImageUrlFunctionName;
protected override Type FunctionResultType => typeof(string);
protected override object GetValue(object rawValue, string baseUrl, int index)
{
return GetImageUrlAt(rawValue, baseUrl, index);
}
}
private sealed class ReportImageSourceFunction : ReportImageFunctionBase
{
public override string Name => ImageSourceFunctionName;
protected override Type FunctionResultType => typeof(ImageSource);
protected override object GetValue(object rawValue, string baseUrl, int index)
{
return GetImageSourceObjectAt(rawValue, baseUrl, index);
}
}
}

View file

@ -0,0 +1,168 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using Sozsoft.Platform;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Enums;
using Sozsoft.Platform.ListForms;
namespace Sozsoft.Reports.PredefinedReports;
internal static class DynamicReportSqlBuilder
{
private const string SourceAlias = "ReportSource";
private const string LookupAlias = "LookupSource";
internal interface IReportLookupColumn
{
ListFormField Field { get; }
string BindingName { get; }
LookupDto Lookup { get; }
Dictionary<string, string> StaticLookupValues { get; }
}
internal static string GetLookupBindingName(ListFormField field)
{
return $"{field.FieldName}__LookupText";
}
internal static bool IsReportQueryableLookup(LookupDto lookup)
{
return lookup.DataSourceType == UiLookupDataSourceTypeEnum.Query &&
!string.IsNullOrWhiteSpace(lookup.LookupQuery);
}
internal static string ApplyLookupTextColumns(
string selectQuery,
IReadOnlyList<IReportLookupColumn> columns,
DefaultValueHelper defaultValueHelper,
DataSourceTypeEnum dataSourceType)
{
if (string.IsNullOrWhiteSpace(selectQuery))
{
return selectQuery;
}
var lookupSelectParts = columns
.Select(column => CreateLookupSelectPart(column, defaultValueHelper, dataSourceType))
.Where(part => !string.IsNullOrWhiteSpace(part))
.ToList();
if (lookupSelectParts.Count == 0)
{
return selectQuery;
}
var sourceAlias = QuoteSqlIdentifier(SourceAlias);
return $"SELECT {sourceAlias}.*, {string.Join(", ", lookupSelectParts)} FROM ({NormalizeSql(selectQuery)}) AS {sourceAlias}";
}
private static string CreateLookupSelectPart(
IReportLookupColumn column,
DefaultValueHelper defaultValueHelper,
DataSourceTypeEnum dataSourceType)
{
if (column.StaticLookupValues?.Count > 0)
{
return CreateStaticLookupSelectPart(column);
}
if (column.Lookup != null && IsReportQueryableLookup(column.Lookup))
{
return CreateQueryLookupSelectPart(column, defaultValueHelper, dataSourceType);
}
return null;
}
private static string CreateStaticLookupSelectPart(IReportLookupColumn column)
{
var sourceField = GetSourceField(column.Field.FieldName);
var cases = column.StaticLookupValues
.Select(item => $"WHEN {ToSqlLiteral(item.Key)} THEN {ToSqlLiteral(item.Value)}");
return $"CASE {sourceField} {string.Join(" ", cases)} ELSE CAST({sourceField} AS VARCHAR(4000)) END AS {QuoteSqlIdentifier(column.BindingName)}";
}
private static string CreateQueryLookupSelectPart(
IReportLookupColumn column,
DefaultValueHelper defaultValueHelper,
DataSourceTypeEnum dataSourceType)
{
var lookupQuery = NormalizeLookupQuery(defaultValueHelper.GetDefaultValue(column.Lookup.LookupQuery));
if (string.IsNullOrWhiteSpace(lookupQuery))
{
return null;
}
var lookupAlias = QuoteSqlIdentifier(LookupAlias);
var valueExpr = QuoteSqlIdentifier(column.Lookup.ValueExpr ?? "Key");
var displayExpr = QuoteSqlIdentifier(column.Lookup.DisplayExpr ?? "Name");
var sourceField = GetSourceField(column.Field.FieldName);
var resultAlias = QuoteSqlIdentifier(column.BindingName);
var scalarQuery = dataSourceType == DataSourceTypeEnum.Mssql
? $"SELECT TOP 1 {lookupAlias}.{displayExpr} FROM ({lookupQuery}) AS {lookupAlias} WHERE {lookupAlias}.{valueExpr} = {sourceField}"
: $"SELECT {lookupAlias}.{displayExpr} FROM ({lookupQuery}) AS {lookupAlias} WHERE {lookupAlias}.{valueExpr} = {sourceField} LIMIT 1";
return $"({scalarQuery}) AS {resultAlias}";
}
private static string GetSourceField(string fieldName)
{
return $"{QuoteSqlIdentifier(SourceAlias)}.{QuoteSqlIdentifier(fieldName)}";
}
private static string NormalizeLookupQuery(string lookupQuery)
{
if (string.IsNullOrWhiteSpace(lookupQuery))
{
return null;
}
var query = lookupQuery.Trim().TrimEnd(';');
query = Regex.Replace(
query,
@"(?<![A-Za-z0-9_])ORDER\s+BY\b[\s\S]*$",
string.Empty,
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
return Regex.Replace(
query,
@"@\s*param\d+",
"NULL",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
}
private static string NormalizeSql(string sql)
{
return sql.Trim().TrimEnd(';');
}
private static string QuoteSqlIdentifier(string identifier)
{
return $"\"{identifier.Replace("\"", "\"\"")}\"";
}
private static string ToSqlLiteral(object value)
{
if (value == null || value == DBNull.Value)
{
return "NULL";
}
return value switch
{
string text => $"'{text.Replace("'", "''")}'",
Guid guid => $"'{guid}'",
DateTime date => $"'{date:yyyy-MM-dd HH:mm:ss.fff}'",
DateTimeOffset date => $"'{date:yyyy-MM-dd HH:mm:ss.fff zzz}'",
bool boolean => boolean ? "1" : "0",
byte or sbyte or short or ushort or int or uint or long or ulong or float or double or decimal
=> Convert.ToString(value, CultureInfo.InvariantCulture),
_ => $"'{value.ToString()?.Replace("'", "''")}'"
};
}
}

View file

@ -3,6 +3,7 @@ using System.IO;
using System.Linq;
using System;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
namespace Sozsoft.Platform.ReportServices;
@ -75,6 +76,79 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens
}
public override byte[] GetData(string url)
{
return GetDataCoreAsync(url).ConfigureAwait(false).GetAwaiter().GetResult();
}
public override Task<byte[]> GetDataAsync(string url)
{
return GetDataCoreAsync(url);
}
private static byte[] SaveReportLayout(XtraReport report)
{
try
{
return SaveReportLayoutCore(report);
}
catch (Exception ex) when (IsGdiPlusError(ex.GetBaseException()))
{
ClearEmbeddedPictureSources(report);
return SaveReportLayoutCore(report);
}
}
private static byte[] SaveReportLayoutCore(XtraReport report)
{
using var ms = new MemoryStream();
report.SaveLayoutToXml(ms);
return ms.ToArray();
}
private static bool IsGdiPlusError(Exception ex)
{
return ex?.Message.Contains("GDI+", StringComparison.OrdinalIgnoreCase) == true;
}
private static void ClearEmbeddedPictureSources(XtraReport report)
{
foreach (var picture in EnumerateControls(report).OfType<XRPictureBox>())
{
if (picture.ImageSource == null || !string.IsNullOrWhiteSpace(picture.ImageUrl))
{
continue;
}
picture.ImageSource = null;
picture.Visible = false;
}
}
private static IEnumerable<XRControl> EnumerateControls(XtraReport report)
{
foreach (Band band in report.Bands)
{
foreach (var control in EnumerateControls(band.Controls))
{
yield return control;
}
}
}
private static IEnumerable<XRControl> EnumerateControls(XRControlCollection controls)
{
foreach (XRControl control in controls)
{
yield return control;
foreach (var child in EnumerateControls(control.Controls))
{
yield return child;
}
}
}
private async Task<byte[]> GetDataCoreAsync(string url)
{
// Uses a specified URL to return report layout data stored within a report storage medium.
// This method is called if the **IsValidUrl** method returns **true**.
@ -86,90 +160,23 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens
if (IsDynamicGridReport(reportName))
{
XtraReport report = HasParameter(parameters, "id")
? Sozsoft.Reports.PredefinedReports.DynamicFormReport.Create(parameters)
: Sozsoft.Reports.PredefinedReports.DynamicGridReport.Create(parameters);
using MemoryStream ms = new MemoryStream();
report.SaveLayoutToXml(ms);
return ms.ToArray();
var report = HasParameter(parameters, "id")
? (XtraReport)await Sozsoft.Reports.PredefinedReports.DynamicFormReport.CreateAsync(parameters)
: await Sozsoft.Reports.PredefinedReports.DynamicGridReport.CreateAsync(parameters);
return SaveReportLayout(report);
}
if (IsDynamicFormReport(reportName))
{
var report = Sozsoft.Reports.PredefinedReports.DynamicFormReport.Create(parameters);
using MemoryStream ms = new MemoryStream();
report.SaveLayoutToXml(ms);
return ms.ToArray();
var report = await Sozsoft.Reports.PredefinedReports.DynamicFormReport.CreateAsync(parameters);
return SaveReportLayout(report);
}
// Önce Reports klasöründen yüklemeyi dene
if (Directory.EnumerateFiles(reportDirectory).Select(Path.GetFileNameWithoutExtension).Contains(reportName))
{
var report = XtraReport.FromFile(Path.Combine(reportDirectory, reportName + FileExtension));
// SQL DataSource'daki filtreyi güncelle
if (parameters.Count > 0)
{
var sqlDataSource = report.ComponentStorage.OfType<DevExpress.DataAccess.Sql.SqlDataSource>().FirstOrDefault();
if (sqlDataSource != null && sqlDataSource.Queries.Count > 0)
{
var query = sqlDataSource.Queries[0] as DevExpress.DataAccess.Sql.SelectQuery;
if (query != null)
{
// FilterString'i oluştur - sadece kolon adını kullan
var filters = new List<string>();
foreach (var param in parameters)
{
// Capitalize first letter for column name matching (Name, Id, etc.)
var columnName = char.ToUpper(param.Key[0]) + param.Key.Substring(1);
filters.Add($"[{columnName}] = '{param.Value.Replace("'", "''")}'");
}
query.FilterString = string.Join(" AND ", filters);
}
}
}
using MemoryStream ms = new MemoryStream();
report.SaveLayoutToXml(ms);
return ms.ToArray();
}
// Reports klasöründe yoksa PredefinedReports namespace'inden yükle
var reportType = Assembly.GetExecutingAssembly()
.GetTypes()
.FirstOrDefault(t => t.Namespace == "Sozsoft.Reports.PredefinedReports" &&
t.Name == reportName &&
typeof(XtraReport).IsAssignableFrom(t));
if (reportType != null)
var reportData = TryGetStoredReportData(reportName, parameters);
if (reportData != null)
{
var report = (XtraReport)Activator.CreateInstance(reportType);
// SQL DataSource'daki filtreyi güncelle
if (parameters.Count > 0)
{
var sqlDataSource = report.ComponentStorage.OfType<DevExpress.DataAccess.Sql.SqlDataSource>().FirstOrDefault();
if (sqlDataSource != null && sqlDataSource.Queries.Count > 0)
{
var query = sqlDataSource.Queries[0] as DevExpress.DataAccess.Sql.SelectQuery;
if (query != null)
{
// FilterString'i oluştur - sadece kolon adını kullan
var filters = new List<string>();
foreach (var param in parameters)
{
// Capitalize first letter for column name matching (Name, Id, etc.)
var columnName = char.ToUpper(param.Key[0]) + param.Key.Substring(1);
filters.Add($"[{columnName}] = '{param.Value.Replace("'", "''")}'");
}
query.FilterString = string.Join(" AND ", filters);
}
}
}
using MemoryStream ms = new MemoryStream();
report.SaveLayoutToXml(ms);
return ms.ToArray();
return reportData;
}
}
catch (Exception ex)
@ -180,6 +187,64 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens
throw new FaultException(new FaultReason(string.Format("Could not find report '{0}'.", url)), new FaultCode("Server"), "GetData");
}
private byte[] TryGetStoredReportData(string reportName, Dictionary<string, string> parameters)
{
var report = TryLoadStoredReport(reportName);
if (report == null)
{
return null;
}
ApplyReportParameters(report, parameters);
return SaveReportLayout(report);
}
private XtraReport TryLoadStoredReport(string reportName)
{
if (Directory.EnumerateFiles(reportDirectory).Select(Path.GetFileNameWithoutExtension).Contains(reportName))
{
return XtraReport.FromFile(Path.Combine(reportDirectory, reportName + FileExtension));
}
var reportType = Assembly.GetExecutingAssembly()
.GetTypes()
.FirstOrDefault(t => t.Namespace == "Sozsoft.Reports.PredefinedReports" &&
t.Name == reportName &&
typeof(XtraReport).IsAssignableFrom(t));
return reportType == null
? null
: (XtraReport)Activator.CreateInstance(reportType);
}
private static void ApplyReportParameters(XtraReport report, Dictionary<string, string> parameters)
{
if (parameters.Count == 0)
{
return;
}
var sqlDataSource = report.ComponentStorage.OfType<DevExpress.DataAccess.Sql.SqlDataSource>().FirstOrDefault();
if (sqlDataSource == null || sqlDataSource.Queries.Count == 0)
{
return;
}
if (sqlDataSource.Queries[0] is not DevExpress.DataAccess.Sql.SelectQuery query)
{
return;
}
var filters = new List<string>();
foreach (var param in parameters)
{
var columnName = char.ToUpper(param.Key[0]) + param.Key.Substring(1);
filters.Add($"[{columnName}] = '{param.Value.Replace("'", "''")}'");
}
query.FilterString = string.Join(" AND ", filters);
}
private static (string ReportName, Dictionary<string, string> Parameters) ParseReportUrl(string url)
{
var decodedUrl = DecodeRepeatedly(url);

View file

@ -11,7 +11,7 @@ import {
FaEye,
FaSave,
FaCog,
FaFileAlt,
FaSistrix,
} from 'react-icons/fa'
import { useNavigate } from 'react-router-dom'
import { GridColumnData } from '../list/GridColumnData'
@ -160,8 +160,6 @@ const FormButtons = (props: {
{hasVisibleToolbarButtons && <Badge innerClass="bg-blue-500" />}
{commandColumnData?.buttons
?.filter((item) => typeof item !== 'string' && !commandExcludedNames.includes(item.name!))
.map((item: any, i) => {
@ -292,7 +290,7 @@ const FormButtons = (props: {
size="sm"
variant="default"
className="dx-button dx-button-mode-contained dx-button-normal"
icon={<FaFileAlt />}
icon={<FaSistrix />}
title={translate('::App.Reports.Form')}
onClick={openDynamicGridReport}
></Button>

View file

@ -6,18 +6,34 @@ import { useLocation, useParams } from 'react-router-dom'
import { RequestOptions } from 'devexpress-reporting-react/dx-report-designer'
import { APP_NAME } from '@/constants/app.constant'
import { createReportUrl } from './reportRouteParams'
import { MODE_DARK } from '@/constants/theme.constant'
import { useStoreState } from '@/store/store'
const ReportDesigner = lazy(() =>
import('devexpress-reporting-react/dx-report-designer')
)
const loadDesignerCss = () => {
const VIEWER_THEME_LINK_ID = 'devexpress-report-viewer-theme'
const VIEWER_DARK_OVERRIDES_ID = 'devexpress-report-viewer-dark-overrides'
const getViewerClassName = (isDarkMode: boolean) =>
[
'dx-viewport',
'dx-device-desktop',
'dx-device-generic',
'dx-theme-generic',
'dx-theme-generic-typography',
isDarkMode ? 'dx-color-scheme-dark' : 'dx-color-scheme-light',
'report-viewer-shell',
isDarkMode ? 'report-viewer-shell-dark' : '',
]
.filter(Boolean)
.join(' ')
const loadViewerCss = (isDarkMode: boolean) => {
const styles = [
new URL('@devexpress/analytics-core/dist/css/dx-analytics.common.css', import.meta.url).href,
new URL('@devexpress/analytics-core/dist/css/dx-analytics.light.css', import.meta.url).href,
new URL('@devexpress/analytics-core/dist/css/dx-querybuilder.css', import.meta.url).href,
new URL('devexpress-reporting/dist/css/dx-webdocumentviewer.css', import.meta.url).href,
new URL('devexpress-reporting/dist/css/dx-reportdesigner.css', import.meta.url).href,
]
styles.forEach((href) => {
@ -27,6 +43,45 @@ const loadDesignerCss = () => {
link.href = href
document.head.appendChild(link)
})
const themeHref = isDarkMode
? new URL('@devexpress/analytics-core/dist/css/dx-analytics.dark.css', import.meta.url).href
: new URL('@devexpress/analytics-core/dist/css/dx-analytics.light.css', import.meta.url).href
let themeLink = document.getElementById(VIEWER_THEME_LINK_ID) as HTMLLinkElement | null
if (!themeLink) {
themeLink = document.createElement('link')
themeLink.id = VIEWER_THEME_LINK_ID
themeLink.rel = 'stylesheet'
document.head.appendChild(themeLink)
}
if (themeLink.href !== themeHref) {
themeLink.href = themeHref
}
if (!document.getElementById(VIEWER_DARK_OVERRIDES_ID)) {
const style = document.createElement('style')
style.id = VIEWER_DARK_OVERRIDES_ID
style.textContent = `
.report-viewer-shell {
min-height: calc(100vh - 4rem);
}
.report-viewer-shell-dark {
background: #111827;
color: #e5e7eb;
}
.report-viewer-shell-dark .dxrd-preview,
.report-viewer-shell-dark .dxrd-preview-wrapper,
.report-viewer-shell-dark .dxrd-toolbar-wrapper,
.report-viewer-shell-dark .dxrd-right-panel,
.report-viewer-shell-dark .dxrd-search-wrapper {
background-color: #111827;
}
`
document.head.appendChild(style)
}
}
const DevexpressReportDesigner: React.FC = () => {
@ -36,17 +91,18 @@ const DevexpressReportDesigner: React.FC = () => {
id: string
listFormCode: string
}>()
const themeMode = useStoreState((state) => state.theme.mode)
const isDarkMode = themeMode === MODE_DARK
const location = useLocation()
const reportUrlWithParams = createReportUrl({ report, id, listFormCode }, location.search, 'design')
useEffect(() => {
loadDesignerCss()
}, [])
loadViewerCss(isDarkMode)
}, [isDarkMode])
if (!reportUrlWithParams) return null
return (
<Container>
<Container className={getViewerClassName(isDarkMode)}>
<Helmet
titleTemplate={`%s | ${APP_NAME}`}
title={translate('::App.Reports')}

View file

@ -1,12 +1,14 @@
import React, { useMemo, useEffect, useState } from 'react'
import React, { useMemo, useEffect, useState, useCallback, useRef } from 'react'
import { Container } from '@/components/shared'
import { Helmet } from 'react-helmet'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { useParams, useLocation } from 'react-router-dom'
import ReportViewer, {
Callbacks,
RequestOptions,
} from 'devexpress-reporting-react/dx-report-viewer'
import { ajaxSetup } from '@devexpress/analytics-core/analytics-utils-native'
import { ZoomAutoBy } from 'devexpress-reporting/viewer/constants'
import { APP_NAME } from '@/constants/app.constant'
import { MODE_DARK } from '@/constants/theme.constant'
import { useStoreState } from '@/store'
@ -14,6 +16,7 @@ import { createReportUrl } from './reportRouteParams'
const VIEWER_THEME_LINK_ID = 'devexpress-report-viewer-theme'
const VIEWER_DARK_OVERRIDES_ID = 'devexpress-report-viewer-dark-overrides'
const DEFAULT_REPORT_ZOOM = 1
const getViewerClassName = (isDarkMode: boolean) =>
[
@ -94,8 +97,43 @@ const DevexpressReportViewer: React.FC = () => {
const token = useStoreState((state) => state.auth.session.token)
const themeMode = useStoreState((state) => state.theme.mode)
const [isRequestAuthReady, setIsRequestAuthReady] = useState(false)
const previewModelRef = useRef<any>(null)
const isDarkMode = themeMode === MODE_DARK
const getReportPreview = useCallback((eventOrModel?: any) => {
const eventArgs = eventOrModel?.args
const sender = eventOrModel?.sender
const candidates = [
eventArgs?.reportPreview,
eventArgs?.ReportPreview,
eventArgs?.previewModel?.reportPreview,
eventOrModel?.reportPreview,
eventOrModel?.ReportPreview,
eventOrModel?.previewModel?.reportPreview,
sender?.GetReportPreview?.(),
sender?.previewModel?.reportPreview,
previewModelRef.current,
]
return candidates.find(
(candidate) =>
candidate &&
('zoom' in candidate ||
'originalZoom' in candidate ||
'autoFitBy' in candidate),
)
}, [])
const setDefaultZoom = useCallback((eventOrModel?: any) => {
const preview = getReportPreview(eventOrModel)
if (!preview) return
previewModelRef.current = preview
preview.autoFitBy = ZoomAutoBy.None
preview.originalZoom = DEFAULT_REPORT_ZOOM
preview.zoom = DEFAULT_REPORT_ZOOM
}, [getReportPreview])
useEffect(() => {
loadViewerCss(isDarkMode)
}, [isDarkMode])
@ -136,6 +174,10 @@ const DevexpressReportViewer: React.FC = () => {
{isRequestAuthReady && (
<ReportViewer reportUrl={reportUrlWithParams}>
<Callbacks
BeforeRender={setDefaultZoom}
DocumentReady={setDefaultZoom}
/>
<RequestOptions
host={import.meta.env.VITE_API_URL}
invokeAction="/DXXRDV"