Continuo fix gestione editing report DevExpress

This commit is contained in:
Samuele Locatelli
2026-04-08 15:58:47 +02:00
parent be16a428f7
commit bccbc9c079
14 changed files with 1706 additions and 67 deletions
+4
View File
@@ -7,6 +7,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DevExpress.AspNetCore.Reporting" />
<PackageReference Include="DevExpress.Blazor" />
<PackageReference Include="DevExpress.Blazor.Reporting.JSBasedControls" />
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" />
<PackageReference Include="Microsoft.EntityFrameworkCore" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
<PrivateAssets>all</PrivateAssets>
@@ -0,0 +1,170 @@
using DevExpress.XtraReports.UI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web;
namespace Lux.Report.Data.Reports
{
public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extensions.ReportStorageWebExtension
{
const string FileExtension = ".repx";
#if false
readonly string reportDirectory = Path.Combine("unsafe_upload", "reports");
public CustomReportStorageWebExtension()
{
if (!Directory.Exists(reportDirectory))
{
Directory.CreateDirectory(reportDirectory);
}
}
#endif
public override bool CanSetData(string url)
{
// Determines whether a report with the specified URL can be saved.
// Add custom logic that returns **false** for reports that should be read-only.
// Return **true** if no valdation is required.
// This method is called only for valid URLs (if the **IsValidUrl** method returns **true**).
return true;
}
public override bool IsValidUrl(string url)
{
// Determines whether the URL passed to the current report storage is valid.
// Implement your own logic to prohibit URLs that contain spaces or other specific characters.
// Return **true** if no validation is required.
return Path.GetFileName(url) == url;
}
/// <summary>
/// Riceve il path completo del report sullo share condiviso
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
public override byte[] GetData(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**.
// You can use the **GetData** method to process report parameters sent from the client
// if the parameters are included in the report URL's query string.
//try
//{
// if (Directory.EnumerateFiles(reportDirectory).Select(Path.GetFileNameWithoutExtension).Contains(url))
// {
// return File.ReadAllBytes(Path.Combine(reportDirectory, url + FileExtension));
// }
//}
//catch (Exception)
//{
// throw new FaultException(new FaultReason("Could not get report data."), new FaultCode("Server"), "GetData");
//}
//throw new FaultException(new FaultReason(string.Format("Could not find report '{0}'.", url)), new FaultCode("Server"), "GetData");
try
{
string reportFolder = Path.GetDirectoryName(url);
string reportName = Path.GetFileName(url);
// Parse the string with the report name and parameter values.
string[] parts = url.Split('?');
#if false
string reportName = parts[0];
#endif
string parametersQueryString = parts.Length > 1 ? parts[1] : String.Empty;
// Create a report instance.
XtraReport report = null;
#if false
if (Directory.EnumerateFiles(reportFolder).
Select(Path.GetFileNameWithoutExtension).Contains(reportName))
{
byte[] reportBytes = File.ReadAllBytes(Path.Combine(reportDirectory, reportName + FileExtension));
using (MemoryStream ms = new MemoryStream(reportBytes))
report = XtraReport.FromStream(ms);
}
#else
byte[] reportBytes = File.ReadAllBytes(Path.Combine(reportFolder, reportName));
using (MemoryStream ms = new MemoryStream(reportBytes))
report = XtraReport.FromStream(ms);
#endif
if (report != null)
{
// Apply the parameter values to the report.
var parameters = HttpUtility.ParseQueryString(parametersQueryString);
foreach (string parameterName in parameters.AllKeys)
{
report.Parameters[parameterName].Value = Convert.ChangeType(
parameters.Get(parameterName), report.Parameters[parameterName].Type);
}
// Disable the Visible property for all report parameters
// to hide the Parameters Panel in the viewer.
foreach (var parameter in report.Parameters)
{
parameter.Visible = false;
}
// If you do not hide the panel, disable the report's RequestParameters property.
// report.RequestParameters = false;
using (MemoryStream ms = new MemoryStream())
{
report.SaveLayoutToXml(ms);
return ms.ToArray();
}
}
}
catch (Exception ex)
{
throw new DevExpress.XtraReports.Web.ClientControls.FaultException(
"Could not get report data.", ex);
}
throw new DevExpress.XtraReports.Web.ClientControls.FaultException(
string.Format("Could not find report '{0}'.", url));
}
#if false
public override Dictionary<string, string> GetUrls()
{
// Returns a dictionary that contains the report names (URLs) and display names.
// The Report Designer uses this method to populate the Open Report and Save Report dialogs.
return Directory.GetFiles(reportDirectory, "*" + FileExtension)
.ToDictionary(x => Path.GetFileNameWithoutExtension(x));
}
public override void SetData(XtraReport report, string url)
{
// Saves the specified report to the report storage with the specified name
// (saves existing reports only).
var resolvedUrl = Path.GetFullPath(Path.Combine(reportDirectory, url + FileExtension));
if (!resolvedUrl.StartsWith(Path.GetFullPath(reportDirectory) + Path.DirectorySeparatorChar))
{
throw new Exception("Invalid report name.");
//throw new FaultException("Invalid report name.");
}
report.SaveLayoutToXml(resolvedUrl);
}
public override string SetNewData(XtraReport report, string defaultUrl)
{
// Allows you to validate and correct the specified name (URL).
// This method also allows you to return the resulting name (URL),
// and to save your report to a storage. The method is called only for new reports.
SetData(report, defaultUrl);
return defaultUrl;
}
#endif
}
}
File diff suppressed because it is too large Load Diff
+43
View File
@@ -0,0 +1,43 @@
using DevExpress.DataAccess.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Lux.Report.Data.Reports
{
public partial class OfferReport : DevExpress.XtraReports.UI.XtraReport
{
public OfferReport()
{
InitializeComponent();
}
public OfferReport(string baseUrl, int offerID)
{
InitializeComponent();
//Parameters["pImgPath"].Value = baseUrl;
Parameters["pOfferId"].Value = offerID;
_offerId = offerID;
_basePath = baseUrl;
RequestParameters = false;
}
private int _offerId = 0;
private string _basePath = "";
protected override void OnDataSourceDemanded(EventArgs e)
{
var jsonDataSource = DataSource as JsonDataSource;
if (jsonDataSource != null && !string.IsNullOrEmpty(_basePath) && _offerId > 0)
{
string url = $"{_basePath}/{_offerId}";
jsonDataSource.JsonSource = new UriJsonSource(new Uri(url));
jsonDataSource.ConnectionName = null;
//RequestParameters = false;
}
}
}
}
+120
View File
@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
@@ -0,0 +1,17 @@
<link href="_content/DevExpress.Blazor.Themes.Fluent/core.min.css" rel="stylesheet" />
<link href="_content/DevExpress.Blazor.Themes.Fluent/global.min.css" rel="stylesheet" />
<link href="_content/DevExpress.Blazor.Themes.Fluent/modes/light.min.css" rel="stylesheet" />
<link href="_content/DevExpress.Blazor.Themes.Fluent/accents/blue.min.css" rel="stylesheet" />
<link href="_content/DevExpress.Blazor.Reporting.Viewer/css/dx-blazor-reporting-components.fluent.css" rel="stylesheet" />
<h3>@reportName</h3>
<div class="small">@ReportPathName</div>
<DxReportDesigner ReportName="@ReportPathName" Height="750px" Width="100%">
<DxReportDesignerCallbacks CustomizeMenuActions="DesignerCustomization.onCustomizeActions"
BeforeRender="ReportingDesignerCustomization.RemoveAppearanceSection"
CustomizeElements="ViewerCustomization.onCustomizeElements" />
</DxReportDesigner>
@@ -0,0 +1,76 @@
using DevExpress.Blazor.Reporting;
using DevExpress.DataAccess;
using DevExpress.DataAccess.Json;
using DevExpress.Drawing.Internal.Fonts.Interop;
using DevExpress.Security;
using DevExpress.XtraReports;
using DevExpress.XtraReports.UI;
using Lux.Report.Data.Reports;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.WebUtilities;
namespace Lux.Report.Manager.Components.Compo
{
public partial class ReportDesigner
{
#region Protected Methods
[Parameter]
public string ReportPathName { get; set; } = "";
private string reportName = "";
private string pdfUrl
{
get => $"download-offer?id={oID}";
}
private int oID = 0;
protected string FileName(string fullName)
{
return Path.GetFileName(fullName);
}
protected override void OnInitialized()
{
// fix nome report
reportName = FileName(ReportPathName);
#if false
// calcolo i parametri x IMG e Json da conf
string apiUrl = _config.GetValue<string>("ServerConf:ApiBaseUrl") ?? "https://iis01.egalware.com/lux/srv/api";
string imgUrl = _config.GetValue<string>("ServerConf:ImageUrl") ?? "image";
string dataUrl = _config.GetValue<string>("ServerConf:DataUrl") ?? "report/offert/";
string imgFullUrl = $"{apiUrl}/{imgUrl}/";
string dataFullUrl = $"{apiUrl}/{dataUrl}";
//cerco id da URL x offerta da mostrare
var uri = NavManager.ToAbsoluteUri(NavManager.Uri);
if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("OfferId", out var offerId))
{
int.TryParse(offerId, out oID);
currReport = new OfferReport(dataFullUrl, oID);
currReport.Parameters["pImgPath"].Value = imgFullUrl;
}
#endif
}
#endregion Protected Methods
#region Private Fields
private OfferReport currReport = new();
private DxReportViewer? reportViewer;
#endregion Private Fields
#region Private Properties
[Inject]
private IConfiguration _config { get; set; } = null!;
[Inject]
private NavigationManager NavManager { get; set; } = null!;
#endregion Private Properties
}
}
@@ -9,14 +9,14 @@
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<NavLink class="nav-link" href="Home" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="report-man">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Report Customize
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Report Customizer
</NavLink>
</div>
@@ -1,4 +1,5 @@
@page "/"
@page "/Home"
<PageTitle>ReportManager</PageTitle>
@@ -2,77 +2,91 @@
<div class="card shadow">
<div class="card-header">
<h3>ReportList</h3>
</div>
<div class="card-body">
@if (isLoading)
{
<p>...loading...</p>
@* <LoadingData></LoadingData> *@
}
else if(!string.IsNullOrEmpty(currRepFile))
{
<span>Edit <b>@currRepFile</b></span>
<button class="btn btn-warning" @onclick="DoCloseEditRep">Chiudi</button>
}
else if (totalCount == 0)
{
<div class="alert alert-info">
<div class="d-flex justify-content-between">
<div class="px-0 fs-4">
Nessun Report trovato
</div>
@if (!string.IsNullOrEmpty(currRepFile))
{
<div class="card-header">
<div class="d-flex justify-content-between">
<div class="px-1 fs-3">
Edit <b>@FileName(currRepFile)</b>
</div>
<div class="px-1">
<button class="btn btn-warning" @onclick="DoCloseEditRep">Chiudi</button>
</div>
</div>
}
else
{
<div class="row">
<div class="@mainCss">
<table class="table table-sm table-striped">
<thead>
<tr>
<th>
<button class="btn btn-sm btn-primary" title="Reset selezione" @onclick="DoReset"><i class="fa-solid fa-arrow-rotate-right"></i></button>
</th>
<th>Nome</th>
<th>Descrizione</th>
<th>RestApi</th>
<th class="text-end"># avail.</th>
</tr>
</thead>
<tbody>
@foreach (var item in ListRecords)
{
<tr class="@CheckSelect(item)">
<td>
<button class="btn btn-sm btn-primary" @onclick="() => DoSelect(item)"><i class="fa-solid fa-magnifying-glass"></i></button>
</td>
<td>@item.Name</td>
<td>@item.Description</td>
<td>@item.JsonDSN</td>
<td class="text-end">@CountNumRep(item.Name)</td>
</div>
<div class="card-body">
<ReportDesigner ReportPathName="@currRepFile"></ReportDesigner>
</div>
}
else
{
<div class="card-header">
<h3>ReportList</h3>
</div>
<div class="card-body">
@if (isLoading)
{
<p>...loading...</p>
@* <LoadingData></LoadingData> *@
}
else if (totalCount == 0)
{
<div class="alert alert-info">
<div class="d-flex justify-content-between">
<div class="px-0 fs-4">
Nessun Report trovato
</div>
</div>
</div>
}
else
{
<div class="row">
<div class="@mainCss">
<table class="table table-sm table-striped">
<thead>
<tr>
<th>
<button class="btn btn-sm btn-primary" title="Reset selezione" @onclick="DoReset"><i class="fa-solid fa-arrow-rotate-right"></i></button>
</th>
<th>Nome</th>
<th>Descrizione</th>
<th>RestApi</th>
<th class="text-end"># avail.</th>
</tr>
}
</tbody>
@* <tfoot>
</thead>
<tbody>
@foreach (var item in ListRecords)
{
<tr class="@CheckSelect(item)">
<td>
<button class="btn btn-sm btn-primary" @onclick="() => DoSelect(item)"><i class="fa-solid fa-magnifying-glass"></i></button>
</td>
<td>@item.Name</td>
<td>@item.Description</td>
<td>@item.JsonDSN</td>
<td class="text-end">@CountNumRep(item.Name)</td>
</tr>
}
</tbody>
@* <tfoot>
<tr>
<td colspan="15">
<EgwCoreLib.Razor.DataPager currPage="@currPage" PageSize="@numRecord" totalCount="@totalCount" numPageChanged="SavePage" numRecordChanged="SaveNumRec"></EgwCoreLib.Razor.DataPager>
</td>
</tr>
</tfoot> *@
</table>
</div>
@if (SelRecord != null)
{
<div class="col-4">
<RepoFileList CurrReport="SelRecord" EC_ReqEdit="DoEditReport"></RepoFileList>
</table>
</div>
}
</div>
}
</div>
@if (SelRecord != null)
{
<div class="col-4">
<RepoFileList CurrReport="SelRecord" EC_ReqEdit="DoEditReport"></RepoFileList>
</div>
}
</div>
}
</div>
}
</div>
@@ -110,5 +110,9 @@ namespace Lux.Report.Manager.Components.Pages
{
currRepFile = "";
}
protected string FileName(string fullName)
{
return Path.GetFileName(fullName);
}
}
}
+4 -1
View File
@@ -8,4 +8,7 @@
@using Microsoft.JSInterop
@using Lux.Report.Manager
@using Lux.Report.Manager.Components
@using Lux.Report.Manager.Components.Compo
@using Lux.Report.Manager.Components.Compo
@using DevExpress.Blazor
@using DevExpress.Blazor.Reporting
@using DevExpress.XtraReports.Web.Extensions
+15 -1
View File
@@ -1,5 +1,8 @@
using DevExpress.Blazor.Reporting;
using DevExpress.XtraReports.Web.Extensions;
using Lux.Report.Data;
using Lux.Report.Data.Repository;
using Lux.Report.Data.Reports;
using Lux.Report.Data.Services;
using Lux.Report.Manager.Components;
using Microsoft.AspNetCore.Localization;
@@ -52,11 +55,20 @@ builder.Services.AddDbContextFactory<ReportContext>(options =>
//builder.Services.AddDatabaseDeveloperPageExceptionFilter();
// aggiungo servizi
// aggiungo servizi "interni"
builder.Services.AddSingleton<IFileService, FileService>();
builder.Services.AddScoped<IReportRepository, ReportRepository>();
builder.Services.AddScoped<IReportService, ReportService>();
// aggiunta servizi DevExpress
builder.Services.AddDevExpressServerSideBlazorReportViewer();
builder.Services.AddMvc();
builder.Services.AddDevExpressBlazorReporting();
builder.WebHost.UseWebRoot("wwwroot");
builder.WebHost.UseStaticWebAssets();
builder.Services.AddScoped<ReportStorageWebExtension, CustomReportStorageWebExtension>();
var app = builder.Build();
@@ -78,6 +90,8 @@ app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
// componenti DevExpress report
app.UseDevExpressBlazorReporting();
// cultura IT...
@@ -1,8 +1,9 @@
{
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
"Microsoft.AspNetCore": "Warning",
}
}
}