Skip to content

workspace sdk report pages

Andre Lafleur edited this page Oct 8, 2026 · 9 revisions

About custom report pages

A custom report page displays tabular data inside the standard Security Desk reporting interface. The reporting interface provides built-in support for filtering, sorting, charting, tile display, and export.

The client-side ReportPage defines the columns and filters. When the operator clicks Generate, the framework sends a CustomQuery to the server-side plugin. The plugin builds a DataTable and returns the rows.

Use a custom report page when operators need the built-in reporting experience with custom server-backed data, filters, charts, or export instead of a generic page or task.

Important

The server-side plugin must include ReportQueryType.Custom in SupportedQueries and the report GUID in SupportedCustomReports. See About plugin queries for handling queries, returning rows, and signaling completion.

ReportPage extends TilePage, which extends Page. It follows the page lifecycle described in About pages, including activation, deactivation, and disposal. However, custom report tasks do not use the generic page Serialize() and Deserialize(byte[]) hooks when the operator saves and reopens the report. To persist custom report state, use the report filter guidance on this page.

Note

Report pages do not require an SDK certificate. The framework skips certificate validation for ReportPage subclasses.

How custom report pages work

  1. The report page class has a [Page] attribute pointing to its descriptor. The module registers a CreatePageTask<T> for the report page type in Load().
  2. When the operator opens the task, the framework creates the report page and calls Initialize(Workspace).
  3. Initialize() is sealed in ReportPage. It prepares the report page for the custom report task.
  4. The framework reads Fields to build the report columns, and reads the filter properties to configure the filter panel.
  5. The operator configures filters and clicks Generate.
  6. The framework sends a CustomQuery to the server-side plugin that matches the report's PageDescriptor.Type.
  7. The plugin returns a DataTable with rows matching the field names.
  8. The reporting interface displays the results in the table, with optional charts and tile display.

Defining report fields

Override Fields to define the columns that appear in the report table. Each ReportField specifies a column name, display name, data type, and display options:

public override List<ReportField> Fields => new List<ReportField>
{
    new ReportField { Type = ReportFieldType.Entity, Name = "Source", DisplayName = "Source", IsSource = true },
    new ReportField { Type = ReportFieldType.DateTime, Name = "Timestamp", DisplayName = "Timestamp", InitialWidth = 180 },
    new ReportField { Type = ReportFieldType.Event, Name = "EventType", DisplayName = "Event" },
    new ReportField { Type = ReportFieldType.Text, Name = "Description", DisplayName = "Description" },
};

ReportField properties

Member Type Description
Type ReportFieldType Data type for the column. Determines how the framework formats and sorts the value.
Name string Unique column identifier without periods. Must match the column name in the DataTable returned by the plugin, including capitalization.
DisplayName string Column header text shown in the report table.
IsSource bool Marks this field as a source for tile display. See below. Defaults to false.
Converter IValueConverter WPF value converter to transform the raw data for display and export. The framework passes the DataRow as the value. The converter is called both when rendering cells in the report table and when exporting to PDF or Excel.
InitialWidth int Default column width in pixels.
IsVisible bool Whether the column is visible by default. Defaults to true. Operators can show hidden columns through the column selection menu. Hidden columns are excluded from exports but remain accessible in EditRow through the DataRow. Use hidden columns for data that supports sorting or row editing without cluttering the report table.
ImageMaxHeight int Maximum image height in pixels. Applies only when Type is Image.
ImageMaxWidth int Maximum image width in pixels. Applies only when Type is Image.

ReportFieldType values

Value Description
Entity An entity reference. The framework resolves the entity name for display.
DateTime A date and time value. Values are interpreted as UTC and displayed in local time.
Event An event type. The framework resolves the event name for display.
Boolean A boolean value.
Text A text string.
Numeric A double-precision numeric value.
Decimal A number with a decimal place.
Image An image. Use ImageMaxHeight and ImageMaxWidth to constrain the display size.
TimeSpan A time duration.

Source fields and tile display

When IsSource is true, the framework uses the field's value to drive tile content. The behavior depends on the field type:

  • Entity fields populate the source entity GUID. When the operator clicks a result row, the tile displays this entity.
  • DateTime fields populate the event timestamp used for playback positioning.
  • Event fields populate the event type for the tile overlay.

Set IsSource to true for at most one Entity field, one DateTime field, and one Event field.

Configuring built-in filters

ReportPage provides virtual properties that toggle built-in filter panels. Override these properties to display the filters relevant to your report:

Property Type Default Description
DisplayEntityFilter bool false Show the entity filter panel.
DisplayEventFilter bool false Show the event type filter panel.
DisplayTimeRangeFilter bool false Show the time range filter panel.
EntityTypes List<EntityType> null Entity types available in the entity filter. Only used when DisplayEntityFilter is true.
EventFilterEntityTypes List<EntityType> null Entity types whose associated events appear in the event filter. Only used when DisplayEventFilter is true.
EventFilterExcluded List<EventType> null Event types to exclude from the event filter. Only used when DisplayEventFilter is true.
protected override bool DisplayEntityFilter => true;
protected override bool DisplayEventFilter => true;
protected override bool DisplayTimeRangeFilter => true;

protected override List<EntityType> EntityTypes => new List<EntityType> { EntityType.Door };
protected override List<EntityType> EventFilterEntityTypes => new List<EntityType> { EntityType.Door };
protected override List<EventType> EventFilterExcluded => new List<EventType> { EventType.DoorClose };

Creating a custom filter

For filtering criteria beyond entities, events, and time ranges, override CustomFilter to provide a custom filter control. The custom filter appears alongside the built-in filters in the filter panel.

Use FilterData to serialize the custom filter state. The framework sends this string to the plugin when the operator runs the report, and restores the same value when the operator reopens a saved report task. Implement the getter to return the current filter state and the setter to restore it.

Create a custom filter by inheriting from ReportFilter, which is a WPF UserControl:

<Pages:ReportFilter
    x:Class="MyModule.MyReportFilter"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Pages="clr-namespace:Genetec.Sdk.Workspace.Pages;assembly=Genetec.Sdk.Workspace">
  <StackPanel Margin="4">
    <TextBlock Text="Priority:" />
    <ComboBox ItemsSource="{Binding Priorities}"
              SelectedValue="{Binding Priority, Mode=TwoWay}" />
  </StackPanel>
</Pages:ReportFilter>
public partial class MyReportFilter : ReportFilter
{
    private int m_priority;

    public MyReportFilter()
    {
        InitializeComponent();
        DataContext = this;
    }

    public IReadOnlyList<int> Priorities { get; } = new[] { 1, 2, 3, 4, 5 };

    public int Priority
    {
        get => m_priority;
        set { m_priority = value; OnModified(); }
    }

    protected override string FilterName => "Priority Filter";

    protected override bool IsMandatory => true;

    protected override bool IsStateValid => Priority > 0;

    protected override string FilterData
    {
        get => Priority.ToString();
        set => Priority = int.TryParse(value, out var p) ? p : 0;
    }
}

Call OnModified() whenever the filter state changes to notify the framework. In this example, setting Priority calls OnModified(), which tells the framework to re-read FilterData and IsStateValid. Because IsMandatory is true, the Generate button is disabled until Priority is greater than zero.

ReportFilter members

Member Type Description
FilterName string (virtual) Display name shown on the filter panel header.
FilterData string (virtual) Current filter state as a serialized string. Included in the query sent to the plugin and persisted when the operator saves the report as a task.
IsMandatory bool (virtual) When true, the report cannot be generated if IsStateValid returns false. Defaults to false.
IsStateValid bool (virtual) Whether the current filter state is valid. When false and IsMandatory is true, the Generate button is disabled. Defaults to true.
IsApplied bool Whether the filter is currently applied. Defaults to true.
Workspace Workspace The workspace instance. Available after Initialize(Workspace) is called.

Override the protected Initialize() method to perform custom setup after the workspace is available.

Then return the filter from your report page:

protected override ReportFilter CustomFilter => new MyReportFilter();

Saving custom report tasks

Operators can save a custom report as a public or private task when the report is eligible and the current operator has the required privileges.

Even though ReportPage derives from Page, the framework does not call Serialize() or Deserialize(byte[]) on the report page when it saves or restores a custom report task.

To persist custom state in a saved report task, store it in ReportFilter.FilterData. The framework saves the filter state and restores it when the operator opens the task again. If your report does not define custom filter state, you do not need to add extra serialization code on the report page.

Do not override CanSave(), Serialize(), or Deserialize(byte[]) on the report page for this scenario. Those hooks apply to generic pages described in About pages, not to custom report task state.

Call OnModified() whenever the filter changes so the framework re-reads FilterData and IsStateValid before the operator generates or saves the report.

Display options

Control whether the report supports chart and tile visualizations:

Property Type Default Description
SupportsCharts bool true Whether the report results can be displayed as a chart.
SupportsTiles bool true Whether the report results can be displayed on tiles.
public override bool SupportsTiles => false;

Handling row editing

Override EditRow to handle double-click on a report row. The ReportPageContext contains the selected rows as DataRow objects:

protected override bool EditRow(ReportPageContext context)
{
    var row = context.SelectedRows.FirstOrDefault();
    if (row == null)
        return false;

    var entityId = (Guid)row["Source"];
    // Open entity details or perform an action
    return true;
}

Return true if the action was handled, false otherwise.

Generating reports programmatically

Call Generate() on a report page instance to execute the query without user interaction. Generate() runs the query with the current filter state and does not check IsMandatory or IsStateValid, so set the filter values you need before you call it:

var page = new MyReport();
page.Initialize(Workspace);
Workspace.ActiveMonitor.Pages.Add(page);
Workspace.ActiveMonitor.ActivePage = page;
page.Generate();

Custom sort order

Override CustomSortDescriptionCollection to define a default sort order for the report results:

protected override SortDescriptionCollection CustomSortDescriptionCollection
{
    get
    {
        var collection = new SortDescriptionCollection();
        collection.Add(new SortDescription("Timestamp", ListSortDirection.Descending));
        return collection;
    }
}

Opening reports through IReportService

Use IReportService to open a built-in report type and prepopulate its filters. Pass the report type GUID, a display name, and a FieldsCollection containing the initial filter values:

var reportService = Workspace.Services.Get<IReportService>();

var fields = new FieldsCollection();
fields.Add("EntityId", doorGuid);

ReportPage page = reportService.CreateReport(reportTypeId, "Door Activity", fields);

An async overload is also available:

ReportPage page = await reportService.CreateReportAsync(reportTypeId, "Door Activity", fields, cancellationToken);

Registering a custom report page

Register the report page by applying the [Page] attribute to the report page class and creating a CreatePageTask<T> in your module's Load method. The descriptor's Type GUID must match the report ID declared in the server-side plugin's SupportedCustomReports property.

The [Page] attribute serves two purposes: it associates the descriptor with the page class, and it makes the report page discoverable by the framework. The client scans deployed Workspace SDK module assemblies for classes with [Page] attributes to find custom report page types. This scanning is independent of the module's Load() method. The server-side plugin assembly that answers the query is deployed separately, on the Security Center servers that run the plugin.

CreatePageTask<T> adds the report task to the Security Desk home page so operators can open it interactively. For details on page descriptors and task categories, see About pages.

[Page(typeof(MyReportDescriptor))]
public class MyReport : ReportPage
{
    public override List<ReportField> Fields => new List<ReportField>
    {
        new ReportField { Type = ReportFieldType.Entity, Name = "Source", DisplayName = "Source", IsSource = true },
        new ReportField { Type = ReportFieldType.DateTime, Name = "Timestamp", DisplayName = "Timestamp", IsSource = true },
        new ReportField { Type = ReportFieldType.Text, Name = "Description", DisplayName = "Description" },
    };

    protected override bool DisplayEntityFilter => true;
    protected override List<EntityType> EntityTypes => new List<EntityType> { EntityType.Door };

    public override bool SupportsTiles => false;
}

public class MyReportDescriptor : PageDescriptor
{
    public override string Name => "My Custom Report";
    public override Guid Type => new Guid("REPORT-TYPE-GUID");
    public override Guid CategoryId => new Guid(TaskCategories.Investigation);
    public override ImageSource Thumbnail => new BitmapImage(new Uri(@"pack://application:,,,/MY-ASSEMBLY;Component/Resources/MY-THUMBNAIL.png", UriKind.RelativeOrAbsolute));
}
public class SampleModule : Module
{
    private CreatePageTask<MyReport> m_reportTask;

    public override void Load()
    {
        if (Workspace.ApplicationType != ApplicationType.SecurityDesk)
            return;

        m_reportTask = new CreatePageTask<MyReport>(isSingleton: true);
        m_reportTask.Initialize(Workspace);
        Workspace.Tasks.Register(m_reportTask);
    }

    public override void Unload()
    {
        if (m_reportTask != null)
            Workspace.Tasks.Unregister(m_reportTask);
    }
}

Report automation

The Report Manager role loads custom modules when executing scheduled report tasks. In this context, Workspace.ApplicationType is ApplicationType.None. The ApplicationType check in the example above prevents the module from registering a CreatePageTask (which requires a home page), but the framework still discovers the report page type through the [Page] attribute on the class.

See also

Platform SDK

Plugin SDK

Workspace SDK

Media SDK

Macro SDK

Web SDK

Synergis RIO

Media Gateway

Genetec Web Player

Clone this wiki locally