workspace sdk content builders - Genetec/DAP GitHub Wiki

About content builders

Content builders control what appears inside tiles across all tile-based pages in Security Desk. This includes the monitoring task, alarm monitoring, and activity/report tasks such as door activity and cardholder activity. Any page that displays tiles uses content builders to produce the visual content for those tiles.

When an entity is dragged onto a tile, an event fires, or an alarm triggers, the system asks registered content builders to produce the content for the tile.

The system iterates through all registered ContentBuilder components sorted by Priority (lowest value first). The first builder that returns a non-null result provides the content for the tile. Builders that return null pass control to the next builder in the chain.

Content builders serve three purposes:

  • Routing: Claim specific entity types and control how they display in tiles
  • Configuration: Set properties on content objects like video mode, playback time, title, and icon
  • Composition: Inject sub-content into tiles built by other content builders

How the tile content system works

When something needs to display in a tile, the system follows this sequence:

  1. The input (entity GUID, event data, alarm data, report row) is converted to a FieldsCollection, a dictionary of field name/value pairs
  2. The system creates a ContentBuilderContext containing the fields and metadata
  3. Registered ContentBuilder components are evaluated in priority order via BuildContentGroup until one returns a non-null result
  4. The first builder that returns a non-null ContentGroup provides the primary tile content
  5. The builder that returned the primary ContentGroup may request sub-content for related entities
  6. When sub-content is requested, registered ContentBuilder components are evaluated in priority order via BuildContent
  7. Returned sub-content is merged into the primary content group
  8. The tile rendering system displays the assembled content

Build methods

Content builders have two build methods, called at different stages:

Method Purpose
BuildContentGroup(ContentBuilderContext) Build the primary content for a tile. Return a ContentGroup or null to pass. Called first during content assembly.
BuildContent(ContentBuilderContext) Build sub-content that gets merged into another builder's tile. Return a Content or null to pass. Called second, only when a primary builder requests sub-content.

Build lifecycle

BuildContentGroup starts primary content selection. The system iterates builders sorted by priority and calls BuildContentGroup until one returns a non-null result.

BuildContent is called second, but not automatically. The builder that returned the primary ContentGroup decides whether to request sub-content for related entities. For example, the built-in door content flow detects a cardholder in the event data, requests sub-content for that cardholder, and evaluates registered content builders through BuildContent.

This is a pull-based, synchronous workflow. The complete sequence happens within a single call chain:

BuildContentGroup called on builders (priority order) until one returns non-null
  └─ Builder returns primary ContentGroup
     └─ Builder may request sub-content for relatedEntityGuid
        └─ BuildContent called on each builder (priority order)
           └─ Returned Content merged into primary ContentGroup

A content builder can implement both methods. BuildContentGroup handles primary content, BuildContent handles sub-content injection. Most custom builders implement only one of the two.

When BuildContent is called

Built-in content flows request sub-content for related entities during tile assembly. The following scenarios trigger BuildContent on registered builders:

Primary content Sub-content requested for Description
Door access event Cardholder or visitor The cardholder who triggered the access event.
Door access event Camera Cameras associated with the door or cardholder.
Access control event Cardholder or visitor The cardholder involved in the event.
Access control event Access point, camera Access points and cameras related to the event.
Elevator event Camera Cameras associated with the elevator.
Custom entity (default handler) Camera children Cameras assigned as children of the custom entity.

If your builder implements BuildContent, it is called during any of these scenarios when the source entity matches what your builder handles.

What BuildContent receives in the context

When BuildContent is called, the context contains:

  • SourceId: The GUID of the related entity (e.g., the cardholder, camera, or access point)
  • Parent: The primary ContentGroup being assembled
  • Fields: The FieldsCollection from the primary content build. The fields belong to the parent content group, not to the SourceId entity. This means BuildContent receives the original event, alarm, or entity fields, not fields specific to the sub-entity being built.

This allows sub-content builders to read event data (timestamps, event types) while also knowing which specific entity the sub-content is being built for.

Modifying the parent content group

BuildContent can modify context.Parent directly instead of returning a new Content. The Parent is a reference to the content group being assembled, so changes to its properties (such as Title, Icon, or Contents) take effect immediately.

public override Content BuildContent(ContentBuilderContext context)
{
    if (context.Parent is EntityContentGroup entityGroup)
    {
        entityGroup.Title = $"{entityGroup.Title} (Modified)";
    }

    return null;
}

When modifying the parent, return null to indicate that no new sub-content is being added. The system collects returned Content objects and merges them into the parent separately, so modifications to the parent and returning a new Content are independent operations.

When BuildContent is called, Parent.Contents does not yet contain sub-content from other builders. The system collects all returned Content objects into a list first, then merges them into the parent after all builders have been called. The parent only contains whatever the primary BuildContentGroup builder put into it initially.

Parent runtime type

Parent is null in BuildContentGroup. In BuildContent, Parent is provided when available, but it can be null when sub-content is built without a parent. Its runtime type depends on what the primary BuildContentGroup builder returned for the tile. Cast Parent to access properties specific to the content scenario:

Scenario Parent value Properties available via cast
Event tile null in default event flows Not applicable
Custom event tile null in default custom event flows Not applicable
Alarm tile AlarmContent when the caller passes the alarm content group EntityId (Guid), InstanceId (int), Priority (int), TriggerTime (DateTime), Location (GeoCoordinate)
public override Content BuildContent(ContentBuilderContext context)
{
    if (context.Parent is EventContent eventContent)
    {
        var eventTime = eventContent.Time;
        var eventType = eventContent.Type;
    }
    else if (context.Parent is AlarmContent alarmContent)
    {
        var alarmPriority = alarmContent.Priority;
        var triggerTime = alarmContent.TriggerTime;
    }

    return null;
}

ContentBuilderContext

The ContentBuilderContext object provides the data needed to build content.

Property Type Description
Fields FieldsCollection Field name/value pairs extracted from the input. Use GetValueOrDefault<T>(fieldName) to read values. Always populated for both build methods.
ContentTypes ContentTypes The type of content being built. Always Unspecified for SDK-registered content builders. Use Fields.Contains() to identify the input type instead.
SourceId Guid The entity GUID for sub-content assembly. Only populated when BuildContent is called. Always Guid.Empty when BuildContentGroup is called.
Parent ContentGroup The parent content group being assembled. Only populated when BuildContent is called. Always null when BuildContentGroup is called.

Note

The ContentTypes enum defines Entity, Event, and Alarm values, but ContentTypes is always Unspecified for SDK-registered content builders. The system uses ContentTypes internally to pre-filter which builders to call before invoking them. SDK-registered builders opt into all content types, so the filtering happens before the builder is called and the value passed to the builder is always Unspecified. Use Fields.Contains() to identify the input type instead, as shown in Identifying the input type.

FieldsCollection by input type

The fields available in context.Fields depend on what triggered the content build. The system converts different input types to a FieldsCollection before calling content builders. There is no universal "Id" field across all input types.

Entity (drag-and-drop)

When an entity is dragged onto a tile, the system creates a FieldsCollection with a single field:

Field name Type Description
"Id" Guid The GUID of the entity dragged onto the tile.

Read the entity GUID:

var entityId = context.Fields.GetValueOrDefault<Guid>("Id");

Event

When an event triggers content building, the FieldsCollection contains fields populated from the event data. Events typically include EventType and EventSubType. Other fields such as EventTimestamp, SourceEntityGuid, Latitude, and Longitude may be present depending on the event source and payload:

Field name Type Description
"SourceEntityGuid" Guid The entity that triggered the event.
"EventType" EventType The type of event.
"EventSubType" int The subtype of the event.
"EventTimestamp" DateTime When the event occurred (UTC).
"Latitude" double Event location latitude. Present only if the event is geographically tagged.
"Longitude" double Event location longitude. Present only if the event is geographically tagged.

Module-specific event extenders add additional fields depending on the event type. For access control events:

Field name Type Description
"CardholderGuid" Guid The cardholder involved in the access event.
"CredentialGuid" Guid The credential used.
"AccessPointGuid" Guid The access point (door side).
"CameraGuid" Guid The camera associated with the event source.

For custom events, event extenders can add any custom fields. See the event extender use case in this document for an example.

Read event fields:

var sourceEntity = context.Fields.GetValueOrDefault<Guid>("SourceEntityGuid");
var eventType = context.Fields.GetValueOrDefault<EventType>("EventType");
var eventTime = context.Fields.GetValueOrDefault<DateTime>("EventTimestamp");
var cameraGuid = context.Fields.GetValueOrDefault<Guid>("CameraGuid");

Note

The "Id" field is not present in event FieldsCollection. Use "SourceEntityGuid" to get the entity that triggered the event.

Alarm

When an alarm triggers content building, the FieldsCollection contains alarm-specific fields:

Field name Type Description
"AlarmGuid" Guid The alarm entity GUID.
"AlarmInstanceID" int The alarm instance identifier.
"AlarmSourceGuid" Guid The source entity that triggered the alarm.
"AlarmPriority" int The alarm priority.
"AlarmTriggerTime" DateTime When the alarm was triggered.
"AlarmState" int The alarm state.
"AlarmCreationTime" DateTime When the alarm event was created.
"EventType" EventType The trigger event data.
"Latitude" double Alarm location latitude, if available.
"Longitude" double Alarm location longitude, if available.

Read alarm fields:

var alarmGuid = context.Fields.GetValueOrDefault<Guid>("AlarmGuid");
var sourceGuid = context.Fields.GetValueOrDefault<Guid>("AlarmSourceGuid");
var triggerTime = context.Fields.GetValueOrDefault<DateTime>("AlarmTriggerTime");
var priority = context.Fields.GetValueOrDefault<int>("AlarmPriority");

Note

The "Id" field is not present in alarm FieldsCollection. Use "AlarmGuid" for the alarm entity or "AlarmSourceGuid" for the entity that triggered the alarm.

Report row

When a report row (activity task) triggers content building, the FieldsCollection is populated from the DataRow columns. The available fields depend on the specific report, but typically include the same field names as events (e.g., "SourceEntityGuid", "EventType", "EventTimestamp", "CameraGuid").

Identifying the input type

To determine what type of input your builder is handling, check which fields are present:

public override ContentGroup BuildContentGroup(ContentBuilderContext context)
{
    if (context.Fields.Contains("AlarmGuid"))
    {
        // Alarm input
    }
    else if (context.Fields.Contains("EventType"))
    {
        // Event or report row input
    }
    else if (context.Fields.Contains("Id"))
    {
        // Entity drag-and-drop input
    }

    return null;
}

DefaultFields constants

The Genetec.Sdk.Reports.Fields.DefaultFields class provides constants for common field names:

Constant String value
DefaultFields.SourceId "SourceEntityGuid"
DefaultFields.EventType "EventType"
DefaultFields.EventTimestamp "EventTimestamp"
DefaultFields.CameraId "CameraGuid"
DefaultFields.CardholderId "CardholderGuid"
DefaultFields.CredentialId "CredentialGuid"
DefaultFields.AlarmId "AlarmGuid"
DefaultFields.CustomEventId "CustomEventID"
DefaultFields.IncidentId "IncidentGuid"
DefaultFields.Latitude "Latitude"
DefaultFields.Longitude "Longitude"

Using constants instead of string literals ensures forward compatibility:

using Genetec.Sdk.Reports.Fields;

var sourceEntity = context.Fields.GetValueOrDefault<Guid>(DefaultFields.SourceId);
var cameraGuid = context.Fields.GetValueOrDefault<Guid>(DefaultFields.CameraId);

Use case: Customizing how a custom entity displays in a tile

Custom entities already display in tiles by default. The system shows the entity icon, name, and video from any assigned cameras. A content builder lets you customize this display.

In this example, the builder intercepts an AED unit custom entity type and sets a descriptive title showing the device status, instead of just the entity name.

public class AedUnitContentBuilder : ContentBuilder
{
    public override string Name => "AED Unit ContentBuilder";

    public override Guid UniqueId { get; } = new("B1A2C3D4-E5F6-7890-ABCD-EF1234567890");

    public override int Priority => 150;

    public override ContentGroup BuildContentGroup(ContentBuilderContext context)
    {
        var entityId = context.Fields.GetValueOrDefault<Guid>("Id");
        if (entityId == Guid.Empty)
            return null;

        var entity = Workspace.Sdk.GetEntity(entityId) as CustomEntity;
        if (entity == null || entity.CustomEntityType != AedUnitCustomEntityType.Id)
            return null;

        var contentGroup = new EntityContentGroup(entityId);
        contentGroup.Initialize(Workspace);
        contentGroup.Title = entity.RunningState == State.Running
            ? $"{entity.Name} (Online)"
            : $"{entity.Name} (Offline)";

        return contentGroup;
    }
}

Default behavior without a content builder

When a custom entity is dragged to a tile with no custom content builder registered, the system:

  • Creates a default content group for the entity
  • Displays the entity icon and name
  • If the entity has camera children, displays their video feeds as sub-content

What the content builder changes

The content builder intercepts the entity before the default handler and returns an EntityContentGroup with a customized title. The cameras still display as sub-content because EntityContentGroup is a built-in type the system already knows how to render. The builder only needs to customize what it cares about (the title) and the rest works automatically.

Required members

Member Description
UniqueId A GUID that uniquely identifies this content builder. Generate a new GUID for each builder.
Name A display name for the component.

Optional members

Member Default Description
Priority int.MaxValue Controls the order in which builders are called. Lower values run first.

Use case: Configuring video playback from a custom event

When a custom event fires on a camera, a content builder can configure how the video tile displays the event. For example, it can set the video to play back at a specific timestamp instead of showing a live feed.

This use case combines three components: a custom event with a JSON payload, an event extender that deserializes the payload into fields, and a content builder that reads those fields to configure the video content.

Raising the custom event

On the Platform SDK side, the analytics system raises a CustomEventInstance on a camera with a JSON payload in ExtraHiddenPayload.

var eventInstance = (CustomEventInstance)engine.ActionManager.BuildEvent(
    EventType.CustomEvent, cameraGuid);
eventInstance.Id = new CustomEventId(analyticsEventId);
eventInstance.ExtraHiddenPayload = new AnalyticsPayload
{
    DetectionTimestamp = DateTime.UtcNow.AddSeconds(-3),
    Label = "Person detected in restricted zone"
}.Serialize();
engine.ActionManager.RaiseEvent(eventInstance);

Extending the event with custom fields

An EventExtender deserializes the JSON payload and populates the FieldsCollection with typed fields.

public class AnalyticsEventExtender : EventExtender
{
    public override IList<Field> Fields { get; } =
    [
        new("DetectionTimestamp", typeof(DateTime)) { Title = "Detection Time", IsDisplayable = true },
        new("DetectionLabel", typeof(string)) { Title = "Label", IsDisplayable = true }
    ];

    public override bool Extend(Event @event, FieldsCollection fields)
    {
        if (@event is CustomEventInstance instance
            && AnalyticsPayload.TryDeserialize(instance.ExtraHiddenPayload, out var payload))
        {
            fields["DetectionTimestamp"] = payload.DetectionTimestamp;
            fields["DetectionLabel"] = payload.Label;
            return true;
        }
        return false;
    }
}

Building video content with playback at detection time

The content builder reads the detection timestamp from the fields and returns a VideoContent configured for playback at that time.

public override Content BuildContent(ContentBuilderContext context)
{
    var timestamp = context.Fields.GetValueOrDefault<DateTime>("DetectionTimestamp");
    if (timestamp == default)
        return null;

    var cameraGuid = context.Fields.GetValueOrDefault<Guid>("CameraGuid");
    if (cameraGuid == Guid.Empty)
        return null;

    var content = new VideoContent(cameraGuid);
    content.Initialize(Workspace);
    content.VideoMode = VideoMode.Playback;
    content.SetPlaybackTimestamp(timestamp);
    content.SetLooping(timestamp.AddSeconds(-5), timestamp.AddSeconds(10));
    return content;
}

Without this content builder, the event tile shows a live video feed. With it, the tile automatically plays a 15-second video loop centered on the detection moment, so the operator sees exactly what triggered the alert.

VideoContent properties

Content builders can configure the following properties on VideoContent:

Member Type Description
VideoMode VideoMode Set to Live or Playback.
SetPlaybackTimestamp(DateTime) Method Jump to a specific playback time.
SetLooping(DateTime, DateTime) Method Loop playback between two timestamps.
Seek(DateTime) Method Seek to a specific time.

Use case: Injecting sub-content into door access event tiles

When a visitor badges at a door, the built-in door content builder creates a tile showing the cardholder photo, name, and camera feeds. A content builder can inject additional content into this tile, such as visitor details read from custom fields.

During tile assembly, the door content flow requests sub-content for the cardholder involved in the access event. Registered content builders are then evaluated through BuildContent(), with context.SourceId set to the cardholder GUID.

public class VisitorContentBuilder : ContentBuilder
{
    public override string Name => "Visitor ContentBuilder";

    public override Guid UniqueId { get; } = new("A1B2C3D4-E5F6-7890-ABCD-EF1234567890");

    private CustomField m_companyField;

    protected override void Initialize()
    {
        var config = (SystemConfiguration)Workspace.Sdk.GetEntity(
            SystemConfiguration.SystemConfigurationGuid);
        var customFieldService = config.CustomFieldService;

        m_companyField = customFieldService.CustomFields
            .FirstOrDefault(cf => cf.Name == "Company"
                               && cf.EntityType == EntityType.Visitor);
    }

    public override Content BuildContent(ContentBuilderContext context)
    {
        if (context.SourceId == Guid.Empty || m_companyField == null)
            return null;

        var visitor = Workspace.Sdk.GetEntity(context.SourceId) as Visitor;
        if (visitor == null)
            return null;

        var config = (SystemConfiguration)Workspace.Sdk.GetEntity(
            SystemConfiguration.SystemConfigurationGuid);
        var company = config.CustomFieldService.GetValue(
            m_companyField, visitor.Guid) as string;
        if (string.IsNullOrEmpty(company))
            return null;

        var escort = Workspace.Sdk.GetEntity(visitor.Escort) as Cardholder;

        var content = new EntityContent();
        content.Initialize(Workspace);
        content.Title = $"Visitor: {company} (Host: {escort?.Name ?? "N/A"})";
        return content;
    }
}

The door event tile now shows an additional tab titled "Visitor: Acme Corp (Host: Jane Smith)" alongside the camera feeds and cardholder photo. For regular cardholders, the builder returns null and adds nothing.

BuildContent context parameters

When BuildContent is called as part of sub-content assembly, the context provides additional information:

Property Description
SourceId The GUID of the entity being processed (e.g., the cardholder involved in the access event).
Parent The parent ContentGroup being assembled. Use this to inspect or modify the parent tile's existing content.

Content class hierarchy

Content builders return objects from two parallel hierarchies: Content and ContentGroup.

A ContentGroup is a container that holds one or more Content objects. Each tile in Security Desk displays exactly one ContentGroup. The Contents property is an ObservableCollection<Content> that holds all the content items in the group. A tile shows one content item at a time: the Current property points to the active item. When the operator cycles between items (for example, switching between a video feed and a map in the same tile), Current changes and the CurrentChanged event fires. Each Content item has a DwellTime property that controls how long it stays visible before the tile automatically advances to the next item in the group.

A Content object represents a single displayable item, such as a video feed, a map, or a web page.

An EntityContentGroup is a ContentGroup that associates the group with a specific entity through its EntityId property. Use EntityContentGroup when the tile represents a known entity. The system has built-in tile views that handle EntityContentGroup, so no custom TileViewBuilder is needed. Use the base ContentGroup when the content is not associated with a single entity or when you build the group manually from unrelated content items.

classDiagram
    class Content {
        <<abstract>>
        +Title : string
        +Icon : ImageSource
        +DwellTime : TimeSpan
        +Initialize(Workspace)
    }

    class EntityContent {
        +EntityId : Guid
    }

    class VideoContent {
        +VideoContent(Guid cameraId)
        +VideoMode : VideoMode
        +Play()
        +Pause()
        +Seek(DateTime)
        +SetPlaybackTimestamp(DateTime)
        +SetLooping(DateTime, DateTime)
    }

    class VideoFileContent {
        +VideoFileContent(string filePath)
        +FilePath : string
        +StartTime : DateTime
        +EndTime : DateTime
    }

    class MapContent {
        +MapContent(Guid map)
        +Center : GeoCoordinate
        +ZoomLevel : double
    }

    class TilePluginContent {
        +TilePluginContent(Guid tilePlugin)
        +Context : string
        +ContextId : Guid
    }

    class WebContent {
        +WebContent(Uri url)
        +Url : string
    }

    class ContentGroup {
        +Title : string
        +Icon : ImageSource
        +Contents : ObservableCollection~Content~
        +Current : Content
        +CurrentChanged : event
        +Initialize(Workspace)
    }

    class EntityContentGroup {
        +EntityContentGroup(Guid entityId)
        +EntityId : Guid
    }

    class SourceContent {
        <<abstract>>
        +SourceId : Guid
    }

    class EventContent {
        +Time : DateTime
        +Type : EventType
    }

    class CustomEventContent {
        +CustomEventId : int
    }

    class AlarmContent {
        <<sealed>>
        +EntityId : Guid
        +InstanceId : int
        +Priority : int
        +TriggerTime : DateTime
        +Location : GeoCoordinate
    }

    Content <|-- EntityContent
    EntityContent <|-- VideoContent
    VideoContent <|-- VideoFileContent
    EntityContent <|-- MapContent
    EntityContent <|-- TilePluginContent
    Content <|-- WebContent

    ContentGroup <|-- EntityContentGroup
    ContentGroup <|-- SourceContent
    SourceContent <|-- EventContent
    EventContent <|-- CustomEventContent
    SourceContent <|-- AlarmContent
Loading

Content types you can create

These classes have public constructors. Content builders create and return instances of these types.

Class Constructor Description
EntityContent EntityContent() Base class for entity-based content. Exposes EntityId.
VideoContent VideoContent(Guid cameraId) Displays live or playback video for a camera. Set VideoMode to Live or Playback. Call SetPlaybackTimestamp() or SetLooping() to configure playback.
VideoFileContent VideoFileContent(string filePath) Plays exported video files (G64, G64X). Exposes StartTime, EndTime, and TimeZone read from the file.
MapContent MapContent(Guid map) Displays a map entity in a tile. Set Center, ZoomLevel, and ViewArea to control the map view.
TilePluginContent TilePluginContent(Guid tilePlugin) Displays a tile plugin entity. Exposes Context and ContextId.
WebContent WebContent(Uri url) Displays a web page in a tile. The constructor sets the URL; to change the page, create a new WebContent instance.
ContentGroup ContentGroup() A container that holds one or more Content objects not associated with a specific entity. Use this when the tile content is assembled from unrelated items.
EntityContentGroup EntityContentGroup(Guid entityId) A ContentGroup associated with a specific entity through EntityId. Use this for most custom entity scenarios. The system has built-in tile views for this type, so no custom TileViewBuilder is needed.

All built-in content types listed above have corresponding tile views registered by the system. Returning any of these types from a content builder works without additional setup. Creating a custom Content subclass requires a matching TileViewBuilder component to be registered, otherwise the system does not know how to render it. For most use cases, returning EntityContentGroup, VideoContent, or one of the other built-in types is sufficient.

Important

Call Initialize(Workspace) on every Content and ContentGroup instance after creating it. Content objects that are not initialized do not function correctly. For example:

var contentGroup = new EntityContentGroup(entityId);
contentGroup.Initialize(Workspace);

Content types you receive

These classes have internal constructors. Content builders do not create them directly but encounter them as context.Parent when BuildContent is called.

Class Description
SourceContent Abstract base for content groups that have a source entity (SourceId).
EventContent Represents an event displayed in a tile. Exposes Time and Type.
CustomEventContent Extends EventContent with CustomEventId for custom events.
AlarmContent Represents an alarm displayed in a tile. Exposes EntityId, InstanceId, Priority, TriggerTime, and Location.

Common Content properties

All Content subclasses share these base properties:

Property Type Description
Title string The display title for this content.
Icon ImageSource The icon displayed for this content.
DwellTime TimeSpan How long this content stays visible when cycling through a group.

All ContentGroup subclasses share these base members:

Member Type Description
Title string The display title for the group.
Icon ImageSource The icon displayed for the group.
Contents ObservableCollection<Content> The collection of content items in this group. Add or remove items to control what the tile can display.
Current Content (get/set) The content item the tile is currently showing. Set this property to switch the tile to a different item in the Contents collection.
CurrentChanged event EventHandler Fires when Current changes, whether set programmatically or by the operator cycling through contents.

Content builders can add Content items to a ContentGroup.Contents collection before returning it from BuildContentGroup. The collection is synchronized with the tile rendering system, so items added at any point are displayed. For example, a builder could create an EntityContentGroup, add a VideoContent and a MapContent to its Contents, and return the group. The tile displays the first content item and the operator can cycle to the next.

Use ContentGroup.Clone() to create a deep copy of an existing content group, including all its contents:

ContentGroup cloned = originalContentGroup.Clone();

The cloned group contains independent copies of each Content item. Modifying the cloned group or its contents does not affect the original.

Priority and builder ordering

Content builders are called in order of their Priority property. Lower values run first.

public override int Priority => 150;

The default priority is int.MaxValue, which places unspecified builders at the end of the chain.

Built-in builder priorities

The system registers built-in content builders at the following priority ranges:

Priority range Handles
8-10 Zones, incidents, doors, access control events
19-30 Intrusion detection, cardholders, visitors, elevators, alarms
40 Video analytics events
50-100 License plate recognition, parking, analog monitors, video, maps, tile layouts
250 Default handler for custom entities

Choosing a priority for your builder

Goal Recommended range Rationale
Intercept a custom entity type before the default handler 101-249 Runs after all built-in entity handlers but before the default custom entity handler at 250.
Replace built-in behavior for a standard entity type Lower than the built-in range for that entity type Runs before the built-in builder, claiming the entity first. Refer to the table above for specific ranges.
Inject sub-content only (via BuildContent) Any value Priority matters less for BuildContent because all builders are called, not just the first match. However, the order they are called still follows priority.
Run after all built-in builders 251 or higher Handles anything the built-in builders did not claim.

For most custom entity use cases, a priority between 101 and 249 is appropriate. This ensures your builder runs after all built-in entity-specific handlers (which top out at 100) but before the default custom entity handler (at 250) can claim the input.

Important

Return null from BuildContentGroup or BuildContent when your builder does not handle the input. Returning null passes control to the next builder. If all builders return null, the tile rejects the content.

If a content builder throws an exception, the build chain stops immediately. The exception propagates to the caller, and no subsequent builders are evaluated. Always guard against exceptions in BuildContentGroup and BuildContent to avoid disrupting the entire content building pipeline.

Build methods execute on the UI thread. Do not perform blocking operations such as network calls or file I/O inside BuildContentGroup or BuildContent. Long-running operations block the UI and degrade the operator experience.

Registering a content builder

Register your content builder in the module's Load method and unregister it in Unload. Content builders are only available in Security Desk. Config Tool does not load content builders because it has no tile-based monitoring pages. Guard your registration with an ApplicationType check:

public class SampleModule : Module
{
    private AedUnitContentBuilder m_contentBuilder;

    public override void Load()
    {
        if (Workspace.ApplicationType == ApplicationType.SecurityDesk)
        {
            m_contentBuilder = new AedUnitContentBuilder();
            m_contentBuilder.Initialize(Workspace);
            Workspace.Components.Register(m_contentBuilder);
        }
    }

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

Note

Content builders require a certificate file to load. For more information, see About Workspace SDK certificates.

Building tile content programmatically

The IContentBuilderService allows code outside of a content builder to trigger the content building pipeline programmatically. Retrieve the service from the workspace:

var service = Workspace.Services.Get<IContentBuilderService>();

Call Build with an entity GUID to produce a ContentGroup as if the entity had been dragged onto a tile:

ContentGroup contentGroup = service.Build(cameraGuid);

The service iterates through all registered content builders in priority order and returns the first non-null result. If no builder handles the input, it returns null.

For example, a custom map object view can call Build when the operator selects an entity on the map, then display the resulting content in a tile.

Build methods

Method Description
Build(object input) Build a ContentGroup from the specified input. Returns null if no builder handles the input. Equivalent to calling Build(input, ContentTypes.Unspecified).
Build(object input, ContentTypes contentTypes) Build a ContentGroup, restricting which built-in builders participate based on content type. SDK-registered builders are always called regardless of this filter.

Supported input types

The input parameter accepts the following types. The service converts each type to a FieldsCollection before passing it to registered content builders.

Input type How it is converted
Guid Creates a FieldsCollection with a single "Id" field set to the GUID. This is the most common usage.
Entity Extracts the entity's Guid and creates a FieldsCollection with "Id" set to that value.
FieldsCollection Passed directly to content builders without conversion.
DataRow Converts all columns to a FieldsCollection. If the row contains additional parameters (alarm fields, cardholder fields, camera fields), those are extracted and added to the collection.
DataRowView Same as DataRow. The underlying DataRow is extracted first.
string (XML) If no content builder handles the input, the service attempts to deserialize the string as content XML (the same format produced by ContentGroup.Serialize).
// Entity GUID (most common)
ContentGroup content = service.Build(cameraGuid);

// Entity object
Camera camera = Workspace.Sdk.GetEntity(cameraGuid) as Camera;
ContentGroup content = service.Build(camera);

// Pre-built FieldsCollection
var fields = new FieldsCollection();
fields.Add("Id", cameraGuid);
ContentGroup content = service.Build(fields);

// Serialized XML
string xml = existingContentGroup.Serialize(Workspace);
ContentGroup content = service.Build(xml);

ContentTypes filter

The ContentTypes parameter in Build(object input, ContentTypes contentTypes) restricts which built-in builders participate. Built-in builders declare which content types they handle (entity, event, alarm). The filter skips built-in builders that do not match.

Value Effect
ContentTypes.Unspecified All builders participate. This is the default when calling Build(object input).
ContentTypes.Entity Only built-in builders that handle entities participate.
ContentTypes.Event Only built-in builders that handle events participate.
ContentTypes.Alarm Only built-in builders that handle alarms participate.

SDK-registered content builders always participate regardless of this filter, because the SDK adapter opts into all content types.

Warning

Calling IContentBuilderService.Build from within BuildContentGroup or BuildContent without protection causes infinite recursion, because the service iterates through all registered builders, including the one currently executing.

Calling Build from inside a content builder

A content builder can call IContentBuilderService.Build to let the system produce the default content for an entity, then customize the result. This is useful when you want the full content that the system would normally create (including sub-content such as PTZ controls, audio channels, and customizations from other builders) and then modify the title, add extra Content items, or change properties before returning it.

Without this technique, you would need to manually create and assemble every Content object yourself. Calling Build delegates that work to the standard pipeline and gives you a fully assembled ContentGroup to work with.

Use a re-entry guard to prevent infinite recursion. When the service iterates back to your builder during the nested Build call, the guard returns null, so the service skips your builder and continues to the next one. The default custom entity handler at priority 250 then produces the standard content, which is returned to your builder for customization.

Your builder must have a priority lower than 250 so that it intercepts the entity before the default handler. If the priority is 251 or higher, the default handler claims the entity first and your builder is never called.

public override int Priority => 150;

private bool m_building;

public override ContentGroup BuildContentGroup(ContentBuilderContext context)
{
    if (m_building)
        return null;

    var entityId = context.Fields.GetValueOrDefault<Guid>("Id");
    var entity = Workspace.Sdk.GetEntity(entityId) as CustomEntity;
    if (entity == null || entity.CustomEntityType != MyCheckpointType.Id)
        return null;

    m_building = true;
    try
    {
        // Let the system build the default content
        // for this entity. The guard causes this builder
        // to return null during the nested call, so the
        // default handler at priority 250 produces content.
        var service = Workspace.Services.Get<IContentBuilderService>();
        ContentGroup content = service.Build(entityId);
        if (content != null)
        {
            content.Title = $"{entity.Name} - Security Checkpoint";
        }
        return content;
    }
    finally
    {
        m_building = false;
    }
}

The build chain is synchronous and runs on the UI thread, so a simple boolean field is sufficient as a re-entry guard.

Serializing and deserializing content

Content and ContentGroup both provide Serialize and Deserialize methods that convert to and from XML strings.

Serialize a ContentGroup to XML:

string xml = contentGroup.Serialize(Workspace);

Deserialize XML back to a ContentGroup:

ContentGroup contentGroup = ContentGroup.Deserialize(Workspace, xml);

Content has the same methods:

string xml = content.Serialize(Workspace);
Content content = Content.Deserialize(Workspace, xml);

Both methods return null if the serialization service is not available or if the XML is invalid.

Displaying content on a remote monitor

The ActionManager.DisplayInTile method sends serialized content to a specific monitor and tile in Security Desk. This is a Platform SDK method available through Workspace.Sdk.ActionManager. Combined with IContentBuilderService and ContentGroup.Serialize, it enables a complete workflow: build content from an entity, serialize it, and push it to a tile on any monitor.

// Build content from an entity using registered content builders
var service = Workspace.Services.Get<IContentBuilderService>();
ContentGroup contentGroup = service.Build(cameraGuid);

// Serialize and display on monitor 1, tile 1
string xml = contentGroup.Serialize(Workspace);
Workspace.Sdk.ActionManager.DisplayInTile(MONITOR_ID, TILE_ID, xml);

Monitor and tile identifiers

The monitorId parameter is the logical ID of a Monitor entity in Security Center. Retrieve it from the Workspace SDK:

int monitorId = Workspace.Monitors[0].LogicalId;

The tileId parameter is the 1-based index of the tile in the tile layout. When the overload without tileId is used, or when tileId is -1, the content displays in the first empty tile.

// Get the monitor logical ID and tile index
var monitor = Workspace.Monitors[0];
int monitorId = monitor.LogicalId;

var tilePage = monitor.ActivePage as TilePage;
int tileCount = tilePage?.States.Count ?? 0;

DisplayInTile methods

Method Description
DisplayInTile(int monitorId, int tileId, string xmlContent) Display content in a specific tile on a monitor.
DisplayInTile(int monitorId, int tileId, string xmlContent, bool forceDisplay) Display content in a specific tile. When forceDisplay is true and the requested tile does not exist in the active page, Security Desk finds the first opened monitoring page with enough tiles, or opens a new one.
DisplayInTile(int monitorId, string xmlContent) Display content in the first empty tile on a monitor.
DisplayInTile(int monitorId, string xmlContent, bool forceDisplay) Display content in the first empty tile with force display option.

Reading and clearing tile content remotely

Use GetTile to retrieve the XML content of a specific tile, and ClearTile to remove content from a tile:

// Get the content of tile 1 on monitor 1
string tileXml = Workspace.Sdk.ActionManager.GetTile(MONITOR_ID, 1);

// Clear the content of tile 1
Workspace.Sdk.ActionManager.ClearTile(MONITOR_ID, 1);
Method Description
GetTile(int monitorId, int tileId) Get the content of a tile as an XML string. Returns String.Empty if the tile is empty or no response is received. Throws SdkException if an error occurs.
BeginGetTile(int monitorId, int tileId, AsyncCallback callback, object state) Begin an asynchronous operation to get tile content. Complete with EndGetTile.
EndGetTile(IAsyncResult asyncResult) Complete an asynchronous GetTile operation. Returns the tile content as XML.
ClearTile(int monitorId, int tileId) Clear the content of a specific tile.

The XML format represents a TileContentGroup with nested content elements. To display a live camera feed without using IContentBuilderService:

string xml = $@"<TileContentGroup>
  <Contents>
    <VideoContent VideoMode=""Live"" Camera=""{cameraGuid}"" />
  </Contents>
</TileContentGroup>";

Workspace.Sdk.ActionManager.DisplayInTile(MONITOR_ID, TILE_ID, xml);

Note

For details on discovering another user's monitors, sending entities by user GUID, and sending saved tasks, see Security Desk.

Accessing tile content through TileState

Each tile in a TilePage has a TileState that exposes the tile's current content. Access tile states through TilePage.States:

var tilePage = Workspace.Monitors[0].ActivePage as TilePage;
if (tilePage != null)
{
    foreach (TileState state in tilePage.States)
    {
        ContentGroup content = state.Content;
        if (content is EntityContentGroup entityGroup)
        {
            Guid entityId = entityGroup.EntityId;
        }
    }
}

Set content on a tile by assigning to TileState.Content:

var service = Workspace.Services.Get<IContentBuilderService>();
ContentGroup contentGroup = service.Build(entityGuid);

var tilePage = Workspace.Monitors[0].ActivePage as TilePage;
if (tilePage != null && contentGroup != null)
{
    tilePage.States[0].Content = contentGroup;
}

TileState members

Member Type Description
Content ContentGroup (get/set) The content displayed in the tile. Set this to change what the tile displays.
IsSelected bool (get/set) Whether the tile is selected.
IsRecordingIncident bool (get) Whether the tile is recording an incident.
Timestamp DateTime? (get) The UTC time when the content was applied.
ContentChanged event EventHandler Raised when the tile's content changes.
Blink() Method Triggers the blinking animation on the tile.

TilePage members

Member Type Description
Pattern TilePattern (get/set) The tile layout pattern (rows and columns).
States IList<TileState> (get) The list of tile states for each tile in the layout.
SynchronizeVideo bool (get/set) Whether synchronized video playback is enabled.

Creating tile patterns

Use TilePattern.Create to define tile layouts programmatically:

// Simple 2x2 grid
TilePattern pattern = TilePattern.Create(2, 2);

// Custom layout: 3x3 grid where the second tile spans 2x2
var rowSpans = new List<uint> { 1, 2, 1, 1, 1, 1 };
var columnSpans = new List<uint> { 1, 2, 1, 1, 1, 1 };
TilePattern pattern = TilePattern.Create(3, rowSpans, 3, columnSpans);
Method Description
TilePattern.Create(uint rows, uint columns) Create a uniform grid with the specified rows and columns. Maximum 15 rows and 15 columns.
TilePattern.Create(uint rows, IList<uint> rowSpans, uint columns, IList<uint> columnSpans) Create a layout with custom tile sizes. Each element in rowSpans and columnSpans defines the size of one tile. Tiles are counted left to right, top to bottom. The sum of all spans must equal rows times columns. Both lists must have the same number of elements.
TilePattern.Deserialize(string xml) Create a tile pattern from an XML string.

Assign a tile pattern to a TilePage to change the tile layout:

var tilePage = Workspace.Monitors[0].ActivePage as TilePage;
if (tilePage != null)
{
    tilePage.Pattern = TilePattern.Create(2, 3);
}

Rendering content with TileViewBuilder and TileView

TileViewBuilder is a component that provides custom rendering for tile content. When a tile's content changes, the system evaluates registered TileViewBuilder components to find one that supports the content.

How tile view selection works

  1. The tile content changes (through a content builder, TileState.Content, or DisplayInTile)
  2. The system creates a TilePluginContext describing the tile state
  3. Registered TileViewBuilder components are evaluated via IsSupported(TilePluginContext) in priority order
  4. The first builder that returns true creates a TileView via CreateView()
  5. The TileView.Update(TilePluginContext) method is called with the current context
  6. The TileView.View property provides the WPF UIElement displayed in the tile

TilePluginContext

TilePluginContext provides the current state of the tile to tile view builders and tile views:

Property Type Description
Page TilePage The active tile page.
TileState TileState The tile state containing the content. Access TileState.Content to get the ContentGroup.
ContentGroup ContentGroup The content group displayed in the tile.
Content Content The currently active content item within the content group.

TileViewBuilder

To provide custom rendering, implement TileViewBuilder with two methods: IsSupported to check whether your builder handles the content, and CreateView to return a TileView.

public sealed class ButtonTileViewBuilder : TileViewBuilder
{
    public override string Name => "Button TileView";

    public override Guid UniqueId { get; } = new("E0B61287-FEF3-40A1-937B-7C6CA9F06879");

    public override bool IsSupported(TilePluginContext context)
    {
        if (context.TileState.Content is not EntityContentGroup entityGroup)
            return false;

        var entity = Workspace.Sdk.GetEntity(entityGroup.EntityId) as CustomEntity;
        return entity?.CustomEntityType == MyCustomEntityType.Id;
    }

    public override TileView CreateView()
    {
        return new ButtonTileView(Workspace);
    }
}

Register a TileViewBuilder the same way as a ContentBuilder, in the module's Load method using Workspace.Components.Register.

TileView

TileView provides the WPF control displayed in the tile. Override Update to react when the tile content changes.

Member Type Description
View UIElement (abstract, get) The WPF element rendered in the tile.
Placement Placement (get/set) Where the view appears on the tile.
Update(TilePluginContext) Method (virtual) Called when the tile content changes. Read context.Content, context.ContentGroup, or context.TileState to update the view.

Placement options

Value Description
Normal The view is constrained under the tile toolbar.
Extended The view takes the full tile area under the tile toolbar.
OverVideo The view is placed as an overlay on top of the video feed.

When a TileViewBuilder is required

Built-in content types (VideoContent, MapContent, EntityContent, WebContent, TilePluginContent) have tile views registered by the system. Returning these types from a content builder works without additional setup.

Creating a custom Content subclass requires a matching TileViewBuilder to be registered. Without one, the system does not know how to render the content.

ContentBuilder members

Member Type Description
UniqueId Property (abstract) Gets the unique identifier for this content builder.
Name Property (abstract) Gets the display name for this component.
Priority Property (virtual) Gets the builder ordering priority. Default is int.MaxValue.
Type Property Gets the component type GUID (always Components.ContentBuilder).
IsAvailable Property Gets or sets whether the builder is available. Defaults to false. Set to true after initialization to make the builder active.
Workspace Property Gets the workspace after initialization.
BuildContentGroup(ContentBuilderContext) Method (virtual) Build primary content for a tile. Returns ContentGroup or null.
BuildContent(ContentBuilderContext) Method (virtual) Build sub-content for another builder's tile. Returns Content or null.
Initialize() Method (virtual) Override to perform setup. Called when Initialize(Workspace) is invoked.
⚠️ **GitHub.com Fallback** ⚠️