Listing pages can offer an Export action that downloads the listed data as a CSV file. The action is displayed in the listing header, next to the search box and header actions, and opens a dialog where users select which columns to include.
Export is disabled by default. Enable it by calling the EnableExport extension method on PageConfiguration within ConfigurePage.
using System.Threading.Tasks;
using Kentico.Xperience.Admin.Base;
public class UserObjectListing : ListingPage
{
// ...
public override Task ConfigurePage()
{
// Adds the 'Export' action to the listing header
PageConfiguration.EnableExport();
return base.ConfigurePage();
}
}The parameterless EnableExport call uses the default export configuration, described in the ListingExportConfiguration reference below. To adjust any of the defaults, pass a ListingExportConfiguration object instead.
PageConfiguration.EnableExport(new ListingExportConfiguration
{
FileName = "users",
MaxRowCount = 10000,
});The export produces a CSV file whose contents depend on the configuration of the listing page, which determines which columns can be exported and how their values are written, and the state of the listing when the user exports it, which determines the rows and columns that end up in the file.
The Export data dialog offers every column that can be exported, all selected by default:
The file contains the columns that the user leaves selected, in the order in which they are configured: the table columns first, followed by the additional columns. The first row of the file contains the column captions.
The file contains all rows that match the search term and filter applied at the moment of the export. The rows are sorted the same way as the listing.
The number of exported rows is limited by the MaxRowCount property (50,000 by default, see Reference - ListingExportConfiguration). When the matching rows exceed the limit, the Export action is disabled and the system never exports a partial result. If this occurs, users need to narrow down the listing using search or filters first.
Values in Localizable columns are resolved via localization macros, and the result is passed to the column's Formatter. Additionally:
DateTime values are converted from the server's time zone to the time zone of the user's browser and formatted as YYYY-MM-DD HH:MM:SS. If the browser's time zone cannot be determined, the values remain in the server's time zone.=, +, -, @, a tab, or a carriage return) are prefixed with an apostrophe ('), so that exported data cannot execute as a formula. Numeric values are exported unchanged, so negative numbers keep their sign.Columns rendered by React components provided by Xperience, such as statuses, tags, links, switches, and languages, export the text the component displays without any additional configuration.
Columns rendered by custom components by default export the value stored in the corresponding column data instead. To export values as formatted and displayed in the admin UI, implement a cell value extractor.
ComponentCellValueExtractor<TProps>, where TProps is the type of the component model produced by the column's modelRetriever function.GetValue method and return the value you want in the exported file – the displayed text, or a typed value (for example a number) when preserving the value type in the file is preferable.RegisterListingCellValueExtractor assembly attribute. The first parameter is the full name of the React component, i.e., the same value passed as componentKey to AddComponentColumn.using System.Threading;
using System.Threading.Tasks;
using Kentico.Xperience.Admin.Base;
// Registers the extractor for the cells rendered by the 'MyComponent' React component
[assembly: RegisterListingCellValueExtractor("@orgName/projectName/MyComponent", typeof(AssignedRolesCellValueExtractor))]
// Extracts the exported value from the props of the rendered component
public class AssignedRolesCellValueExtractor : ComponentCellValueExtractor<ReactComponentToDisplayProps>
{
public override Task<object> GetValue(ReactComponentToDisplayProps componentProps, CancellationToken cancellationToken = default)
{
return Task.FromResult<object>(componentProps.prop1);
}
}The following rules apply to extractors:
GetValue – prepare the data of the whole result set using a DataModifier instead.Use AdditionalColumns to export data that the listing table does not display. Each entry is a ColumnConfiguration object, configured the same way as the columns of the table.
PageConfiguration.EnableExport(new ListingExportConfiguration
{
AdditionalColumns =
[
new ColumnConfiguration
{
Name = nameof(UserInfo.UserGUID),
Caption = "User GUID"
}
]
});The columns appear in the Export data dialog after the columns of the table, where users select them like any other exported column.
The columns are retrieved together with the table columns, which places the following requirements on them:
LoadedExternally set to true.AdditionalColumns.To adjust exported values that depend on other data, for example names of related objects, assign a DataModifier. The function receives all exported rows at once. Each row is an IDataContainer holding the columns of the listed object, accessible using indexer notation.
The rows the modifier receives hold only the columns retrieved by the listing. You can change the values of these columns, but setting any other column on them has no effect. To export a new column, return new rows that include it:
AdditionalColumns with LoadedExternally set to true.DataContainer, copy the row's columns into it, and set the value of the new column.The following example exports the number of contacts in each contact group from the Contact groups application.
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CMS.Base;
using CMS.ContactManagement;
using CMS.DataEngine;
using Kentico.Xperience.Admin.Base;
using Kentico.Xperience.Admin.DigitalMarketing.UIPages;
[assembly: PageExtender(typeof(ContactGroupListExtender))]
public class ContactGroupListExtender : PageExtender<ContactGroupList>
{
private const string CONTACT_COUNT_COLUMN = "ContactCount";
private readonly IInfoProvider<ContactGroupMemberInfo> contactGroupMemberProvider;
public ContactGroupListExtender(IInfoProvider<ContactGroupMemberInfo> contactGroupMemberProvider)
{
this.contactGroupMemberProvider = contactGroupMemberProvider;
}
public override Task ConfigurePage()
{
Page.PageConfiguration.EnableExport(new ListingExportConfiguration
{
// Declares the exported column filled by the data modifier
// The column does not exist in the database, so it is loaded externally
AdditionalColumns =
[
new ColumnConfiguration
{
Name = CONTACT_COUNT_COLUMN,
Caption = "Number of contacts",
LoadedExternally = true
}
],
DataModifier = AddContactCounts
});
return base.ConfigurePage();
}
// Adds the number of contacts in each exported contact group
private async Task<IEnumerable<IDataContainer>> AddContactCounts(IEnumerable<IDataContainer> rows, CancellationToken cancellationToken)
{
var exportedRows = rows.ToList();
var contactGroupIds = exportedRows.Select(row => (int)row[nameof(ContactGroupInfo.ContactGroupID)]).ToList();
// Counts the contact members of all exported contact groups in a single query
var contactCounts = (await contactGroupMemberProvider.Get()
.Columns(
new QueryColumn(nameof(ContactGroupMemberInfo.ContactGroupMemberContactGroupID)),
new CountColumn(nameof(ContactGroupMemberInfo.ContactGroupMemberID)).As(CONTACT_COUNT_COLUMN))
.WhereIn(nameof(ContactGroupMemberInfo.ContactGroupMemberContactGroupID), contactGroupIds)
.WhereEquals(nameof(ContactGroupMemberInfo.ContactGroupMemberType), (int)ContactGroupMemberTypeEnum.Contact)
.GroupBy(nameof(ContactGroupMemberInfo.ContactGroupMemberContactGroupID))
.GetDataContainerResultAsync(cancellationToken: cancellationToken))
.ToDictionary(
count => (int)count[nameof(ContactGroupMemberInfo.ContactGroupMemberContactGroupID)],
count => (int)count[CONTACT_COUNT_COLUMN]);
// The retrieved rows only hold the columns selected by the listing query, so each row
// is copied into a container that can also hold the additional column
return exportedRows.Select(row =>
{
var container = new DataContainer();
foreach (var columnName in row.ColumnNames)
{
container[columnName] = row[columnName];
}
container[CONTACT_COUNT_COLUMN] = contactCounts.GetValueOrDefault((int)row[nameof(ContactGroupInfo.ContactGroupID)]);
return (IDataContainer)container;
}).ToList();
}
}The modifier runs after the data is retrieved and before the CSV file is built.
To add the export to a listing page provided by Xperience, call EnableExport from a page extender of the page. Extenders access the configuration of the listing through their Page property.
using System.Threading.Tasks;
using CMS.ContactManagement;
using Kentico.Xperience.Admin.Base;
using Kentico.Xperience.Admin.DigitalMarketing.UIPages;
[assembly: PageExtender(typeof(ContactListExtender))]
public class ContactListExtender : PageExtender<ContactList>
{
public override Task ConfigurePage()
{
// Enables the export of the contact listing with the specified configuration
Page.PageConfiguration.EnableExport(new ListingExportConfiguration
{
FileName = "contacts",
// Exports the identifier of each contact, which the listing table does not display
AdditionalColumns =
[
new ColumnConfiguration
{
Name = nameof(ContactInfo.ContactGUID),
Caption = "Contact GUID"
}
]
});
return base.ConfigurePage();
}
}EnableExport replaces the entire export configuration of the page. On listing pages that already offer the export, such as form submissions, calling it from an extender discards the existing configuration.
By default, the export requires the SystemPermissions.VIEW permission, so anyone who can open the listing can export it. Set Permission to require a different permission – typically a custom permission dedicated to exporting, which lets you grant read access to a listing without allowing bulk extraction of its data.
PageConfiguration.EnableExport(new ListingExportConfiguration
{
Permission = "Export"
});The permission must be defined for the application containing the listing page. Users without the permission see the Export action disabled, with a tooltip explaining that the permission is missing.
The system raises the AfterExportListingEvent event (available in the Kentico.Xperience.Admin.Base namespace) after every successful export, on every listing page that has the export enabled. Handle the event for scenarios such as auditing or compliance logging, where you need a record of who exported which data and when.
The event uses the asynchronous event model described for content events. To handle the event, create a class implementing the IAsyncEventHandler<AfterExportListingEvent> interface (from the CMS.Base namespace) and define its HandleAsync method. Access the event object's Data property to get an ExportListingEventData object with the following properties:
Property | Description |
|---|---|
| The full type name of the listing page whose data was exported. For example, exports performed in the Forms application report Kentico.Xperience.Admin.DigitalMarketing.UIPages.FormSubmissionsTab. Use this value to distinguish between listing pages when multiple applications have the export enabled. |
| The ID of the user who performed the export. The value is 0 if the system cannot determine the user. |
| The local time of the server at which the export was completed. |
All properties of the event data are read-only and the event does not provide access to the exported file or its content. Handlers cannot modify or cancel the export.
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using CMS.Base;
using Kentico.Xperience.Admin.Base;
// Handler triggered after a user exports the data of a listing page
public class ExportAuditHandler : IAsyncEventHandler<AfterExportListingEvent>
{
private readonly ILogger<ExportAuditHandler> logger;
// Gets an ILogger instance using constructor DI
public ExportAuditHandler(ILogger<ExportAuditHandler> logger)
{
this.logger = logger;
}
public Task HandleAsync(AfterExportListingEvent asyncEvent, CancellationToken cancellationToken)
{
// Accesses the strongly-typed event data
ExportListingEventData data = asyncEvent.Data;
// Logs the details of the export
// The 'name' value of the EventId object sets the event code in the Xperience event log
logger.LogInformation(
new EventId(0, "EXPORT"),
"User {UserID} exported '{ListingPageTypeName}' at {Timestamp}.",
data.UserID,
data.ListingPageTypeName,
data.Timestamp);
return Task.CompletedTask;
}
}The example uses the Information log level when logging events. Information level events are not written to the Xperience event log by default. To include them, set the log level for the event category in the XperienceEventLog logging provider. See Configure logging.
Register the handler by calling the AddEventHandler<TAsyncEvent, THandler>() extension method on IServiceCollection (opens in a new tab). Handlers are registered as singletons, so you can resolve any required services using constructor injection.
using CMS.Base;
using Kentico.Xperience.Admin.Base;
// ...
// Registers the handler for the AfterExportListingEvent event
builder.Services.AddEventHandler<AfterExportListingEvent, ExportAuditHandler>();Property | Description |
|---|---|
FileName | Name of the exported file, without the extension. The system appends the Derived from the localized name of the listing page, if not specified – characters not allowed in file names and white space are replaced by hyphens (for example, the Event log page produces Event-log.csv). |
MaxRowCount | Maximum number of rows a single export can contain. Defaults to 50,000. When the listing contains more rows than the limit, the Export action is disabled and displays a tooltip stating that the limit was exceeded. Users need to narrow the listing down using the search box or the filter before they can export. Note: Be careful when setting the limit. All exported rows are retrieved and held in memory before the file is built, so a high limit increases the memory consumption and the time users wait for the download, and may cause the request to time out. Keep the limit as low as your scenario allows and let users narrow the listing down via filters instead. |
Permission | Permission required to export the data. Defaults to |
AdditionalColumns | Columns included in the export without being displayed in the listing table. Exported after the table columns. Defaults to an empty collection. See Export columns that are not displayed in the table. |
DataModifier | Function that transforms the whole exported result set before it is written to the file. Defaults to null, i.e., the data is exported as retrieved. See Transform exported values. |