Skip to main content

Dynamic Form Helper Pattern

Overview

DynamicFormHelper is a static JSON utility used in Protrak programs to read, merge, and transform Dynamic Form template and response payloads.

When to Use

  • Merge two or more form templates.
  • Append multiple forms into single-page or multi-page output.
  • Read or update nested values without writing custom recursive traversal logic.
  • Build validation or migration utilities for form JSON.
  • Extract labels and keys for reporting or dynamic processing.

Applicable Program Types

The helper is static and can be used from any program type:

  • IPreCreateTriggerProgramAsync
  • IPreUpdateTriggerProgramAsync
  • IPostCreateTriggerProgramAsync
  • IPromoteActionCommandProgramAsync
  • ISchedularProgramAsync
  • ICommonProgramAsync

Core Methods

MethodPurposeExample
CombineJsonObjects(params string[] dynamicFormJsons)Deep-merge one or more JSON objectsDynamicFormHelper.CombineJsonObjects(dynamicFormJson1, dynamicFormJson2)
AppendJsonObjects(string[] dynamicFormJsons, bool isCombineInSinglePage)Append form pages into one or many pagesDynamicFormHelper.AppendJsonObjects(dynamicFormJsons, true)
SetPropertyValue(string jsonText, string propertyPath, object value)Set value at path (creates missing parent objects)SetPropertyValue(json, "pages[0].title", "Step 1")
GetPropertyValue(string jsonText, string propertyPath, object defaultValue = null)Read value at path with fallbackGetPropertyValue(json, "title", "")
PropertyExists(string jsonText, string propertyPath)Check whether a path existsPropertyExists(json, "pages[0].name")
GetPropertyPathError(string jsonText, string propertyPath)Returns a friendly path validation error or nullGetPropertyPathError(json, "pages[0].elements[0].name")
GetAllPropertyKeys(string jsonText, bool includeArrayIndices = false)Flatten all paths in JSONGetAllPropertyKeys(json, true)
FindAndReplaceValue(string jsonText, object oldValue, object newValue)Replace matching values recursivelyFindAndReplaceValue(json, false, true)
ExtractNameTitlePairs(string jsonText, string preferredLocale = null)Build localized name -> title lookup dictionaryExtractNameTitlePairs(templateJson, "en-US")
ExtractPropertyArrays(string jsonText, params string[] propertyNames)Extract and combine top-level arraysExtractPropertyArrays(json, "a", "b")
ArrayContainsValue(string jsonText, string arrayPath, object value)Check value in array pathArrayContainsValue(json, "supportedLocales", "en")

Merge Behavior

CombineJsonObjects performs deep merge:

  • Nested objects are merged recursively.
  • Arrays are appended.
  • Scalar conflicts use last value.

AppendJsonObjects supports page-level composition:

  • isCombineInSinglePage: true: all elements are flattened into one page.
  • isCombineInSinglePage: false: pages are kept separate and renamed sequentially (page1, page2, ...).
  • Supports both page-based forms (pages[].elements) and root-level question structures (elements / questions).
  • Throws an error if no usable pages/elements/questions are found.

Important Note on Uniqueness

  • When combining or appending forms, question name values must be unique across the final JSON.
  • CombineJsonObjects and AppendJsonObjects do not auto-rename duplicate question names.
  • If duplicates are possible, normalize names before merge/append (for example: Safety_q1, General_q1).

Deep Merge Example

Input jsonA:

{
"title": "Form A",
"settings": { "showProgressBar": "top" },
"pages": [
{ "name": "page1", "elements": [{ "name": "qA1", "type": "text" }] }
]
}

Input jsonB:

{
"description": "Form B description",
"settings": { "locale": "en" },
"pages": [
{ "name": "page2", "elements": [{ "name": "qB1", "type": "rating" }] }
]
}

Deep merge:

var deepMerged = DynamicFormHelper.CombineJsonObjects(dynamicFormJsonA, dynamicFormJsonB);

Deep merge result (key points):

{
"title": "Form A",
"description": "Form B description",
"settings": {
"showProgressBar": "top",
"locale": "en"
},
"pages": [
{ "name": "page1", "elements": [{ "name": "qA1", "type": "text" }] },
{ "name": "page2", "elements": [{ "name": "qB1", "type": "rating" }] }
]
}

Append JSON Example

Single page append:

var singlePage = DynamicFormHelper.AppendJsonObjects(new[] { dynamicFormJsonA, dynamicFormJsonB }, true);

Expected result behavior:

  • Final JSON has only one page.
  • Elements from all input pages are collected into that single page.

Separate pages append:

var separatePages = DynamicFormHelper.AppendJsonObjects(new[] { dynamicFormJsonA, dynamicFormJsonB }, false);

Expected result behavior:

  • Final JSON keeps all pages.
  • Page names are reassigned sequentially (page1, page2, ...).
  • Ensure element name values remain unique across all pages.

Code Example

using System;
using System.Linq;
using Prorigo.Protrak.API.Contracts.Helpers;

public async Task<ProgramResult> RunAsync(Instance instance, string fromState, string toState, string actionName)
{
try
{
// Read two template JSON payloads from attributes.
var dynamicFormJsonA = instance.GetTextAttributeValue("STANDARDDynamicFormTemplateJson");
var dynamicFormJsonB = instance.GetTextAttributeValue("SecondaryDynamicFormTemplateJson");

// Deep merge templates using the current API (no deepMerge parameter).
var mergedTemplate = DynamicFormHelper.CombineJsonObjects(dynamicFormJsonA, dynamicFormJsonB);

// Add metadata.
mergedTemplate = DynamicFormHelper.SetPropertyValue(mergedTemplate, "title", "Inspection Form");
mergedTemplate = DynamicFormHelper.SetPropertyValue(mergedTemplate, "showProgressBar", "top");

// Locale-aware title extraction.
var nameTitleMap = DynamicFormHelper.ExtractNameTitlePairs(mergedTemplate, "en-US");
bool hasLocalizedLabels = nameTitleMap.Values.Any(v => !string.IsNullOrWhiteSpace(v));

instance.SetTextAttributeValue("STANDARDDynamicFormTemplateJson", mergedTemplate);
return new ProgramResult { IsSuccess = true };
}
catch (ArgumentException ex)
{
return new ProgramResult
{
IsSuccess = false,
Errors = new[] { ex.Message }
};
}
catch (InvalidOperationException ex)
{
return new ProgramResult
{
IsSuccess = false,
Errors = new[] { ex.Message }
};
}
}