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:
IPreCreateTriggerProgramAsyncIPreUpdateTriggerProgramAsyncIPostCreateTriggerProgramAsyncIPromoteActionCommandProgramAsyncISchedularProgramAsyncICommonProgramAsync
Core Methods
| Method | Purpose | Example |
|---|---|---|
CombineJsonObjects(params string[] dynamicFormJsons) | Deep-merge one or more JSON objects | DynamicFormHelper.CombineJsonObjects(dynamicFormJson1, dynamicFormJson2) |
AppendJsonObjects(string[] dynamicFormJsons, bool isCombineInSinglePage) | Append form pages into one or many pages | DynamicFormHelper.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 fallback | GetPropertyValue(json, "title", "") |
PropertyExists(string jsonText, string propertyPath) | Check whether a path exists | PropertyExists(json, "pages[0].name") |
GetPropertyPathError(string jsonText, string propertyPath) | Returns a friendly path validation error or null | GetPropertyPathError(json, "pages[0].elements[0].name") |
GetAllPropertyKeys(string jsonText, bool includeArrayIndices = false) | Flatten all paths in JSON | GetAllPropertyKeys(json, true) |
FindAndReplaceValue(string jsonText, object oldValue, object newValue) | Replace matching values recursively | FindAndReplaceValue(json, false, true) |
ExtractNameTitlePairs(string jsonText, string preferredLocale = null) | Build localized name -> title lookup dictionary | ExtractNameTitlePairs(templateJson, "en-US") |
ExtractPropertyArrays(string jsonText, params string[] propertyNames) | Extract and combine top-level arrays | ExtractPropertyArrays(json, "a", "b") |
ArrayContainsValue(string jsonText, string arrayPath, object value) | Check value in array path | ArrayContainsValue(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
namevalues must be unique across the final JSON. CombineJsonObjectsandAppendJsonObjectsdo 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
namevalues 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 }
};
}
}