workspace sdk content builders - Genetec/DAP GitHub Wiki
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
When something needs to display in a tile, the system follows this sequence:
- The input (entity GUID, event data, alarm data, report row) is converted to a
FieldsCollection, a dictionary of field name/value pairs - The system creates a
ContentBuilderContextcontaining the fields and metadata - Registered
ContentBuildercomponents are evaluated in priority order viaBuildContentGroupuntil one returns a non-null result - The first builder that returns a non-null
ContentGroupprovides the primary tile content - The builder that returned the primary
ContentGroupmay request sub-content for related entities - When sub-content is requested, registered
ContentBuildercomponents are evaluated in priority order viaBuildContent - Returned sub-content is merged into the primary content group
- The tile rendering system displays the assembled content
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. |
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.
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.
When BuildContent is called, the context contains:
-
SourceId: The GUID of the related entity (e.g., the cardholder, camera, or access point) -
Parent: The primaryContentGroupbeing assembled -
Fields: TheFieldsCollectionfrom the primary content build. The fields belong to the parent content group, not to theSourceIdentity. This meansBuildContentreceives 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.
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 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;
}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.
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.
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");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.
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.
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").
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;
}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);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;
}
}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
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.
| 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. |
| Member | Default | Description |
|---|---|---|
Priority |
int.MaxValue |
Controls the order in which builders are called. Lower values run first. |
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.
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);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;
}
}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.
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. |
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.
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 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
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);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. |
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.
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.
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 |
| 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.
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.
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.
| 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. |
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);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.
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.
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.
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);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;| 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. |
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.
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;
}| 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. |
| 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. |
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);
}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.
- The tile content changes (through a content builder,
TileState.Content, orDisplayInTile) - The system creates a
TilePluginContextdescribing the tile state - Registered
TileViewBuildercomponents are evaluated viaIsSupported(TilePluginContext)in priority order - The first builder that returns
truecreates aTileViewviaCreateView() - The
TileView.Update(TilePluginContext)method is called with the current context - The
TileView.Viewproperty provides the WPFUIElementdisplayed in the tile
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. |
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 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. |
| 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. |
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.
| 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. |