This example shows how to use the End-User Report Designer's client-side API to customize the Save As and Open dialogs.
- To customize the Save As dialog, handle the
CustomizeSaveAsDialog
event and call thee.Customize
method. - To customize the Open dialog, handle the
CustomizeOpenDialog
event and call thee.Customize
method.
Customize the Save As Dialog
Report files in this example are in folders in the root Reports folder. Folder names correspond to the report's category. The customized dialog displays report names and categories. The dialog allows users to do the following:
- Select the existing category and file name, and save the report
- Enter the category name and create a new category
- Enter the file name and save a report with a new name.
The dialog also displays reports that do not fall in any category - reports created by the ReportsFactory static class and reports in the root Reports folder.
Dialog Template
The Save As dialog is defined in an HTML template. The template contains the following widgets:
- The
TextBox
editor is bound to the model'sinputValue
property and displays the report's name. - The
SelectBox
editor is bound to the model's categories property and displays the category name. - The
List
widget is bound to the model's categories property and displays reports grouped by category. The categories data is an array of keys (category names) associated with multiple values (report names). The dxListBox requires this structure to display grouped values.
Dialog Model
The dialog model defines the properties used in the dialog template and binds them to Knockout observables. The model specifies the following functions:
- To set the current report URL
- To get the current report URL
- To update the model's properties when the dialog is displayed. The
updateCategories
JavaScript function is used.
The updateCategories
function calls the client-side ReportStorageWeb.getUrls
method to obtain report names and categories. This method uses the ReportStorageWebExtension.GetUrls
method of server-side report storage to get a dictionary that contains report names and categories. The code processes the dictionary and fills the categories data array.
The model defines the dialog buttons and their actions. The Save button's action calls the e.Popup.save
method and the Cancel button's action calls the e.Popup.cancel
method.
The dialog HTML template and dialog model are passed to the e.Customize
method to modify the Report Designer's Save As dialog. This method is available in the CustomizeSaveAsDialog
event handler.
CustomizeSaveAsDialog event
The customizeSaveAsDialog
function is the CustomizeSaveAsDialog
event handler. The function uses the event handler argument’s Popup
property to specify the dialog’s width, height, and title. The function defines variables used in the dialog model and defines the dialog model. Finally, the function calls the e.Customize
method to modify the dialog based on the specified model and template.
The ReportDesignerClientSideEventsBuilder.CustomizeSaveAsDialog
method is used to set the name of the JavaScript function that handles the CustomizeSaveAsDialog
event (the customizeSaveAsDialog
function in this example).
Customize the Open Dialog
The custom Open dialog allows the user to find a report in a list grouped by category, select the report, and open it. The user can type in the text box to filter the list and find report and category names that contain the input string.
The Open dialog is customized in the same way as the Save As dialog. The ReportDesignerClientSideEventsBuilder.CustomizeOpenDialog
method specifies the name of the JavaScript function that handles the CustomizeOpenDialog
event - the customizeOpenDialog
function in this example.
The Open dialog template and the dialog model are defined and passed to the e.Customize
method to modify the dialog.
Files to Review
Documentation
More Examples
- ASP.NET Core Reporting - Best Practices
- Custom Report Parameter Types in Web Reporting Controls (ASP.NET Core)
- Web Reporting (ASP.NET MVC, ASP.NET Core and Angular) - How to Customize the DevExpress Report Wizard Customization and Hide Data Source Actions within our Report Designer
Does this example address your development requirements/objectives?
(you will be redirected to DevExpress.com to submit your response)
Example Code
Razor<link rel="stylesheet" href="~/css/viewer.part.bundle.css" />
<link rel="stylesheet" href="~/css/designer.part.bundle.css" />
<link rel="stylesheet" href="~/css/ace/ace.bundle.css" />
<script src="~/js/viewer.part.bundle.js"></script>
<script src="~/js/designer.part.bundle.js"></script>
<style>
.dxrd-reportdialog-content .dx-list-collapsible-groups .dx-list-group:first-child .dx-list-group-header {
display: none;
}
.dxrd-reportdialog-content .dx-list-collapsible-groups .dx-list-item {
border-top: 0;
}
.dx-designer .dx-popup-bottom.dx-toolbar .dx-toolbar-items-container {
margin-left: 6px;
}
</style>
<script>
function updateCategories(url, categories, koCategory) {
DevExpress.Reporting.Designer.ReportStorageWeb.getUrls().done(function(result) {
var categoryArray = [{ key: "", items: [] }];
(result || []).forEach(function(reportItem) {
var parts = reportItem.Value.split('\\');
var reportName = parts.pop();
var categoryName = parts.length > 0 ? parts.join('\\') : "";
var category = categoryArray.filter(function(item) { return item.key === categoryName; })[0];
if(!category) {
category = { key: categoryName, items: [] };
categoryArray.push(category);
}
category.items.push({ text: reportItem.Key, displayName: reportName, onClick: function() { url(reportItem.Key); koCategory && koCategory(categoryName); } });
})
categories(categoryArray);
});
}
function customizeSaveAsDialog(s, e) {
e.Popup.width("700px");
e.Popup.height("522px");
e.Popup.title = "Save";
var categories = ko.observableArray([]);
var koUrl = ko.observable("");
var koInput = ko.observable("");
var koCategory = ko.observable("");
koUrl.subscribe(function(newVal) {
newVal = newVal.replace('/', '\\');
var paths = newVal.split('\\');
var fileName = paths.pop();
koInput(fileName);
var catName = paths.join('\\');
koCategory(catName);
});
var updateReportName = function(reportName) {
koUrl(koCategory() ? (koCategory() + '\\' + reportName) : reportName);
};
koCategory.subscribe(function(newVal) {
newVal = newVal.replace('/', '\\');
updateReportName(koInput());
});
updateCategories(koUrl, categories);
var onCustomCategoryCreating = function(data) {
if(!data.text || data.text === "none") {
data.customItem = null;
return;
}
data.customItem = { key: data.text, items: [] };
categories.push(data.customItem);
koUrl(data.text + '\\' + koInput());
}
var model = {
categories: categories,
categoryName: koCategory,
reportUrl: koUrl,
onReportNameChanged: function(e) {
updateReportName(e.value);
},
onCustomCategoryCreating: onCustomCategoryCreating,
inputValue: koInput,
categoryDisplayExpr: function(item) {
return item && item.key || "none";
},
setUrl: function(url) {
koUrl(url);
},
getUrl: function() {
return koUrl();
},
onShow: function(tab) {
koInput("");
updateCategories(koUrl, categories, koCategory);
},
popupButtons: [
{
toolbar: 'bottom', location: 'after', widget: 'dxButton', options: {
text: 'Save', onClick: function() {
if(!koInput()) return;
e.Popup.save(koUrl());
}
}
},
{
toolbar: 'bottom', location: 'after', widget: 'dxButton', options: {
text: 'Cancel', onClick: function() {
e.Popup.cancel();
}
}
}
]
}
e.Customize("custom-save-as-dialog", model);
}
function customizeOpenDialog(s, e) {
e.Popup.width("700px");
e.Popup.height("476px");
e.Popup.title = "Open";
var categories = ko.observableArray([]);
var koUrl = ko.observable("");
var koInput = ko.observable("");
updateCategories(koUrl, categories);
var model = {
categories: categories,
reportUrl: koUrl,
inputValue: koInput,
setUrl: function(url) {
koUrl(url);
},
getUrl: function() {
return koUrl();
},
onShow: function(tab) {
koInput("");
updateCategories(koUrl, categories);
},
popupButtons: [
{
toolbar: 'bottom', location: 'after', widget: 'dxButton', options: {
text: 'Open', onClick: function() {
e.Popup.open(koUrl());
}
}
},
{
toolbar: 'bottom', location: 'after', widget: 'dxButton', options: {
text: 'Cancel', onClick: function() {
e.Popup.cancel();
}
}
}
]
}
e.Customize("custom-open-dialog", model)
}
</script>
<style>
.dxrd-reportdialog-content .reportdialog-item.dx-texteditor:not(.dx-multiline):not(.dx-textarea) {
height: 36px;
margin-bottom: 10px;
}
</style>
<script type="text/html" id="custom-save-as-dialog">
<div class="dxrd-reportdialog-content">
<div style="margin-bottom: 10px;"
data-bind="dxTextBox: {
height: 36,
value: inputValue,
valueChangeEvent: 'keyup',
onValueChanged: onReportNameChanged,
placeholder: 'Enter a report name to save...',
showClearButton: true
}"></div>
<div style="margin-bottom: 10px;"
data-bind="dxSelectBox: {
height: 36,
dataSource: categories,
value: categoryName,
valueExpr: 'key',
displayExpr: categoryDisplayExpr,
acceptCustomValue: true,
placeholder: 'Select a category...',
onCustomItemCreating: onCustomCategoryCreating
}"></div>
<div class="dx-default-border-style dxd-border-secondary"
data-bind="dxList: {
dataSource: categories,
height: '260px',
grouped: true,
displayExpr: 'displayName',
keyExpr: 'text',
collapsibleGroups: true,
}"></div>
</div>
</script>
<script type="text/html" id="custom-open-dialog">
<div class="dxrd-reportdialog-content">
<div style="margin-bottom: 10px;" data-bind="dxTextBox: { height: 36, value: inputValue, valueChangeEvent: 'keyup', placeholder: 'Enter text to search...', showClearButton: true }"></div>
<div class="dx-default-border-style dxd-border-secondary"
data-bind="dxList: {
dataSource: categories,
height: '260px',
grouped: true,
searchExpr: 'text',
searchValue: inputValue,
displayExpr: 'displayName',
keyExpr: 'text',
collapsibleGroups: true,
}"></div>
</div>
</script>
@(Html.DevExpress().ReportDesigner("reportDesigner")
.Height("1000px")
.Bind(@"Category1\Report1")
.ClientSideEvents(configure => {
configure.CustomizeSaveAsDialog("customizeSaveAsDialog");
configure.CustomizeOpenDialog("customizeOpenDialog");
}));
C#using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AspNetCoreReportingDialogCustomization.PredefinedReports;
using DevExpress.XtraReports.UI;
using Microsoft.AspNetCore.Hosting;
namespace AspNetCoreReportingDialogCustomization.Services {
public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extensions.ReportStorageWebExtension {
readonly string ReportDirectory;
const string FileExtension = ".repx";
public CustomReportStorageWebExtension(IWebHostEnvironment env) {
ReportDirectory = Path.Combine(env.ContentRootPath, "Reports");
if(!Directory.Exists(ReportDirectory)) {
Directory.CreateDirectory(ReportDirectory);
}
}
private bool IsWithinReportsFolder(string url, string folder) {
var rootDirectory = new DirectoryInfo(folder);
var fileInfo = new FileInfo(Path.Combine(folder, url));
return fileInfo.Directory.FullName.ToLower().StartsWith(rootDirectory.FullName.ToLower());
}
public override bool CanSetData(string url) {
// Determines whether or not it is possible to store a report by a given URL.
// For instance, make the CanSetData method return false for reports that should be read-only in your storage.
// This method is called only for valid URLs (i.e., if the IsValidUrl method returned true) before the SetData method is called.
return true;
}
public override bool IsValidUrl(string url) {
// Determines whether or not the URL passed to the current Report Storage is valid.
// For instance, implement your own logic to prohibit URLs that contain white spaces or some other special characters.
// This method is called before the CanSetData and GetData methods.
var rootDirectory = new DirectoryInfo(ReportDirectory);
var fileInfo = new FileInfo(Path.Combine(ReportDirectory, url));
return fileInfo.Directory.FullName.ToLower().StartsWith(rootDirectory.FullName.ToLower());
}
public override byte[] GetData(string url) {
// Returns report layout data stored in a Report Storage using the specified URL.
// This method is called only for valid URLs after the IsValidUrl method is called.
try {
var filename = url.Split('\\').Last();
var fullPath = Path.Combine(ReportDirectory, url + FileExtension);
if(File.Exists(fullPath)) {
return File.ReadAllBytes(fullPath);
}
if(ReportsFactory.Reports.ContainsKey(url)) {
using(MemoryStream ms = new MemoryStream()) {
ReportsFactory.Reports[url]().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));
}
public override Dictionary<string, string> GetUrls() {
// Returns a dictionary of the existing report URLs and display names.
// This method is called when running the Report Designer,
// before the Open Report and Save Report dialogs are shown and after a new report is saved to a storage.
var repxFiles = new DirectoryInfo(ReportDirectory).GetFiles("*" + FileExtension, SearchOption.AllDirectories);
var dictionary = repxFiles
.Select(x => {
var directory = x.Directory.FullName == ReportDirectory ? "" : x.Directory.FullName.Substring(ReportDirectory.Length + 1);
return Path.Combine(directory, Path.GetFileNameWithoutExtension(x.Name));
})
.ToDictionary(k => k, v => v);
foreach(var predefinedReportList in ReportsFactory.Reports)
if(!dictionary.ContainsKey(predefinedReportList.Key))
dictionary.Add(predefinedReportList.Key, predefinedReportList.Key);
return dictionary;
}
public override void SetData(XtraReport report, string url) {
// Stores the specified report to a Report Storage using the specified URL.
// This method is called only after the IsValidUrl and CanSetData methods are called.
if(!IsWithinReportsFolder(url, ReportDirectory))
throw new DevExpress.XtraReports.Web.ClientControls.FaultException("Invalid report name.");
var parts = url.Split('\\');
var newDirectory = ReportDirectory + "\\" + parts[0];
if(parts.Length == 2 && !Directory.Exists(newDirectory)) {
Directory.CreateDirectory(newDirectory);
}
report.SaveLayoutToXml(Path.Combine(ReportDirectory, url + FileExtension));
}
public override string SetNewData(XtraReport report, string defaultUrl) {
// Stores the specified report using a new URL.
// The IsValidUrl and CanSetData methods are never called before this method.
// You can validate and correct the specified URL directly in the SetNewData method implementation
// and return the resulting URL used to save a report in your storage.
SetData(report, defaultUrl);
return defaultUrl;
}
}
}
C#using DevExpress.XtraReports.UI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace AspNetCoreReportingDialogCustomization.PredefinedReports
{
public static class ReportsFactory
{
public static Dictionary<string, Func<XtraReport>> Reports = new Dictionary<string, Func<XtraReport>>()
{
["TestReport"] = () => new TestReport()
};
}
}
C#using System;
using System.IO;
using DevExpress.AspNetCore;
using DevExpress.AspNetCore.Reporting;
using DevExpress.DataAccess.Excel;
using DevExpress.DataAccess.Sql;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using DevExpress.XtraReports.Web.Extensions;
using AspNetCoreReportingDialogCustomization.Services;
namespace AspNetCoreReportingDialogCustomization {
public class Startup {
public Startup(IConfiguration configuration, IWebHostEnvironment hostingEnvironment) {
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services) {
services.AddDevExpressControls();
services.AddScoped<ReportStorageWebExtension, CustomReportStorageWebExtension>();
services
.AddControllersWithViews()
.AddNewtonsoftJson();
services.ConfigureReportingServices(configurator => {
configurator.ConfigureReportDesigner(designerConfigurator => {
designerConfigurator.RegisterDataSourceWizardConfigFileConnectionStringsProvider();
});
configurator.ConfigureWebDocumentViewer(viewerConfigurator => {
viewerConfigurator.UseCachedReportSourceBuilder();
});
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) {
DevExpress.XtraReports.Configuration.Settings.Default.UserDesignerOptions.DataBindingMode = DevExpress.XtraReports.UI.DataBindingMode.Expressions;
app.UseDevExpressControls();
System.Net.ServicePointManager.SecurityProtocol |= System.Net.SecurityProtocolType.Tls12;
if(env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
} else {
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints => {
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}