BlazorDynamicForm
Automatically generate fully interactive Blazor forms from C# classes using type annotations — no hand-written markup required.
BlazorDynamicForm is an internal library that turns annotated C# model classes into complete, editable UI forms at runtime. It relies on two cooperating layers: the TypeAnnotationParser, which reflects over a class to produce a portable schema (SchemeModel), and the DynamicForm Blazor component, which reads that schema and renders the appropriate input controls. The schema can also be serialized to YAML for storage or external editing, then deserialized back at runtime.
Architecture overview
The system is split into two NuGet-style projects:
| Project | Responsibility |
|---|---|
| TypeAnnotationParser | Reflects a C# Type, walks its properties and attributes, and outputs a SchemeModel — a tree of SchemeProperty nodes that describes every field, its data type, nesting, and attached metadata attributes. |
| BlazorDynamicForm | Consumes a SchemeModel and renders a Blazor component tree. Each PropertyType is mapped to a Razor component (e.g., StringComponent, IntComponent, ObjectComponent). Custom attributes can override the default renderer. |
┌────────────────────┐ ┌─────────────────────┐ ┌──────────────┐
│ C# Class + Attrs │──parse─▶ SchemeModel (JSON/ │──bind──▶ <DynamicForm │
│ (or YAML config) │ │ YAML portable) │ │ Scheme=... │
└────────────────────┘ └─────────────────────┘ │ Data=...> │
└──────────────┘
TypeAnnotationParser
The parser uses .NET reflection to convert any Type into a SchemeModel.
Core types
| Type | Description |
|---|---|
SchemeModel | Root of the schema tree. Extends SchemeProperty and adds a References dictionary for deduplicating complex/nested types. |
SchemeProperty | Describes a single property: its Type (enum), optional Ref pointer, child Properties (for objects), Indices (for arrays/dictionaries), Enum values, and a list of Attributes. |
PropertyType | Enum — String, Integer, Float, Double, Decimal, Boolean, Enum, Object, Array, Dictionary, File. |
AttributeScheme | Base class for all metadata attributes that the parser collects. Custom attributes must inherit from this. |
ParserConfiguration | Configuration record passed to the parser constructor. Contains a list of Annotation entries. |
How parsing works
- Instantiate the parser with a
ParserConfiguration:
var config = new ParserConfiguration();
var parser = new TypeAnnotationParser(config);
- Call
Parse<T>()(orParse(Type)):
SchemeModel scheme = parser.Parse<MyClass>();
- The parser walks every public property of the type:
- Determines the
PropertyTypeusingDeterminePropertyType(). - Collects all attributes that derive from
AttributeSchemevia reflection. - For
Objecttypes, recursively processes child properties. Complex types are stored once inSchemeModel.Referencesand referenced by key (#TypeName) to avoid duplication and infinite recursion. - For
Array/Dictionarytypes, extracts the generic argument(s) and stores them inSchemeProperty.Indices. - A depth limit of 20 prevents infinite recursion on self-referencing types.
- Determines the
Type mapping rules
| C# Type | PropertyType |
|---|---|
string | String |
bool | Boolean |
int, long, short, byte (and unsigned variants) | Integer |
float | Float |
double | Double |
decimal | Decimal |
Any enum | Enum |
T[], List<T>, any IList | Array |
Dictionary<K,V>, any IDictionary | Dictionary |
| Any other class/struct | Object |
Reference deduplication
When the parser encounters a complex Object type that has already been processed, it reuses the existing entry in SchemeModel.References instead of duplicating the definition. Properties that point to a shared type store a Ref string (e.g., #Cube) rather than inlining the full schema.
public class Parent
{
public Child A { get; set; } // both reference "#Child"
public Child B { get; set; }
}
YAML serialization
The SchemeModel can be round-tripped to and from YAML using the TypeAnnotationParser.Serialization.Scheme static class. This enables storing form definitions as configuration files that can be edited outside of code.
Serialize a type to YAML
using TypeAnnotationParser.Serialization;
List<Type> attributeTypes = new()
{
typeof(CodeEditorAttribute),
typeof(MultipleSelectAttribute),
typeof(NameAttribute),
typeof(GridAttribute),
typeof(BoxAttribute),
typeof(LabelAttribute)
};
string yaml = Scheme.GetYamlFromScheme<SwitchTask>(attributeTypes);
Each attribute type is registered as a YAML tag (!CodeEditor, !MultipleSelect, etc.) so that custom metadata survives the round-trip.
Deserialize YAML back to a scheme
SchemeModel? scheme = Scheme.GetSchemeFromYaml(attributeTypes, yaml);
Example YAML output
references:
'#SwitchCase':
name: SwitchCase
type: Object
properties:
Op:
type: String
attributes:
- !Label
name: Op
- !MultipleSelect
options:
- Equals
- Contains
- Regex
- '>'
- '<'
Value:
type: String
attributes:
- !Name
name: Value
Branch:
type: String
attributes:
- !Name
name: Branch
name: SwitchTask
type: Object
properties:
Variable:
type: String
attributes:
- !Name
name: Variable to evaluate
DefaultBranch:
type: String
attributes:
- !Name
name: Default branch
Cases:
type: Array
attributes:
- !Name
name: Cases
indices:
- ref: '#SwitchCase'
BlazorDynamicForm component library
Registration
Register the form services in Program.cs:
builder.Services.AddBlazorDynamicForm();
This registers a singleton DynamicFormConfiguration that maps every PropertyType to a default Blazor component and wires up custom attribute renderers.
Default renderer mappings
PropertyType | Component | UI Control |
|---|---|---|
String | StringComponent | Text input with icon |
Integer | IntComponent | Numeric input |
Float | FloatComponent | Numeric input |
Double | DecimalComponent | Numeric input |
Decimal | DecimalComponent | Numeric input |
Boolean | BooleanComponent | Toggle switch |
Enum | EnumComponent | Dropdown select |
Object | ObjectComponent | Recursive fieldset with grid layout |
Array | ListComponent | Dynamic list with add/remove buttons |
Dictionary | DictionaryComponent | Key-value editor with add/remove |
File | FileComponent | File upload |
Custom attribute renderers
Certain attributes override the default component for a property, regardless of its PropertyType:
| Attribute | Component | Effect |
|---|---|---|
TextAreaAttribute | TextAreaComponent | Renders a multi-line textarea |
CodeEditorAttribute | CodeEditorComponent | Renders a Monaco code editor |
MultipleSelectAttribute | MultipleOptionsComponent | Renders a multi-select dropdown |
Using the <DynamicForm> component
@using BlazorDynamicForm.Components
@using TypeAnnotationParser
<DynamicForm Scheme="@_scheme" Data="_formData" OnValidSubmit="@OnSubmit">
<SubmitTemplate>
<button type="submit" class="btn btn-primary">Save</button>
</SubmitTemplate>
</DynamicForm>
@code {
private SchemeModel _scheme;
private IDictionary<string, object> _formData = new Dictionary<string, object>();
protected override void OnParametersSet()
{
var parser = new TypeAnnotationParser(new ParserConfiguration());
_scheme = parser.Parse<MyConfigClass>();
}
void OnSubmit(IDictionary<string, object>? data)
{
// data contains the form values as a dictionary tree
var json = JsonConvert.SerializeObject(data, Formatting.Indented);
}
}
Parameters
| Parameter | Type | Description |
|---|---|---|
Scheme | SchemeModel | The parsed schema that defines the form structure. |
Data | IDictionary<string, object> | Two-way bound dictionary holding the form values. Created/validated automatically by ObjectGenerator. |
OnValidSubmit | EventCallback<IDictionary<string, object>?> | Fired when the form is submitted and all validation rules pass. |
OnDataChanged | EventCallback<IDictionary<string, object>> | Fired whenever any field value changes (debounced 300ms). Use for auto-save or live sync. |
SubmitTemplate | RenderFragment | Custom markup for the submit button area. |
Attributes reference
All custom attributes inherit from AttributeScheme (metadata-only) or DynamicRendererComponent (overrides the rendered component).
Layout & display attributes
| Attribute | Base | Properties | Description |
|---|---|---|---|
GridAttribute | AttributeScheme | Size (int, 1–12) | Controls the Bootstrap column width. Default is 12 (full width). |
LabelAttribute | AttributeScheme | Label (string), Position (None / Inline / Top) | Adds a label above or beside the field. |
BoxAttribute | AttributeScheme | Visibility (None / Visible) | Wraps the object in a collapsible card container. |
NameAttribute | AttributeScheme | Name (string) | Overrides the property name displayed in the UI. |
PlaceholderAttribute | AttributeScheme | Placeholder (string) | Sets placeholder text on string inputs. |
TooltipAttribute | AttributeScheme | Text (string) | Shows a native browser tooltip on hover over the field label. |
DocumentationAttribute | AttributeScheme | Description, DocumentationUrl, Example, ExampleLanguage | Attaches an expandable documentation panel (book icon) next to the label. See Documentation attribute. |
CsvImportAttribute | AttributeScheme | LabelColumn, ValueColumn, LabelProperty, ListProperty, ValueProperty | On a List<T> field, adds an "Import CSV" button that parses a long-format CSV client-side and replaces the rows. Grouped (ListProperty set) or flat. 5 MB cap. |
Renderer-override attributes
| Attribute | Base | Properties | Description |
|---|---|---|---|
TextAreaAttribute | DynamicRendererComponent | — | Forces a multi-line textarea renderer. |
CodeEditorAttribute | DynamicRendererComponent | Language, Theme, Example, Templated, Variables | Forces a Monaco code editor. Language sets syntax highlighting (e.g., "javascript", "csharp", "json"). Set Templated = true (or supply Variables) to register a Monaco completion provider that pops the workflow's declared variables on { trigger and inserts them as {name}. |
MultipleSelectAttribute | DynamicRendererComponent | Options (string[]) | Forces a multi-select dropdown with the given choices. |
SliderAttribute | DynamicRendererComponent | Step (double, default 0.01) | Renders a numeric field as a range slider paired with a numeric readout. Bounds come from [Range]; the bound property may be float / double / decimal / int / long. |
SelectBoxAttribute | DynamicRendererComponent | Options (string[]) | Forces a single-select dropdown (defined in TypeAnnotationParser). |
DataProviderAttribute | DynamicRendererComponent | Path (string), ValueField (string), LabelField (string) | Fetches selectable options from an HTTP endpoint. See Data Provider section. |
EnvironmentPickerAttribute | DynamicRendererComponent | — | Two-step cascading picker (project → env) for choosing a workspace environment. Binds a Guid? env id. See Environment Picker section. |
Validation & value attributes
| Attribute | Base | Properties | Description |
|---|---|---|---|
RequiredRule | ValidationRule | — | Validates the field is not null, empty, or whitespace. Evaluated on form submit. |
RangeAttribute | AttributeScheme | Min, Max | Constrains numeric values to a min/max range. Supports int, float, and decimal. |
DefaultValueFormAttribute | AttributeScheme | DefaultValue (object) | Sets a default value when ObjectGenerator creates data for a null field. |
ReadonlyFormAttribute | AttributeScheme | — | Makes the field read-only. Applied to all input components (StringComponent, IntComponent, BooleanComponent, etc.). |
ObjectGenerator — data creation & validation
The ObjectGenerator extension method CreateOrValidateData walks a SchemeModel and either creates a fresh data dictionary matching the schema or validates/repairs an existing one.
// Create empty data matching the schema
object data = scheme.CreateOrValidateData(scheme, null);
// Validate existing data against the schema
object validated = scheme.CreateOrValidateData(scheme, existingData);
Options
var options = new ObjectGeneratorOptions
{
MaxRecursiveDepth = 10, // prevent infinite recursion
InitStringsEmpty = true, // initialize strings as ""
CreateCollectionElement = false, // auto-add one item to empty lists
CreateDictionaryElement = false, // auto-add one entry to empty dicts
CreateObjectElement = false
};
Behavior per type
| Type | Behavior |
|---|---|
String | Returns existing string or "". If DefaultValueFormAttribute is present and data is null, uses that instead. |
Integer | Returns existing int or 0 |
Float / Double / Decimal | Returns existing value or 0 |
Boolean | Returns existing bool or false |
Enum | Returns existing int index or 0 |
DateTime | Returns existing DateTime or DateTime.MinValue |
Object | Ensures a Dictionary<string, object?>, validates each sub-property, removes unknown keys |
Array | Ensures a List<object?>, optionally creates one element, validates existing items |
Dictionary | Ensures a Dictionary<string, object?>, validates values against the value-type schema |
Validation
The form evaluates all ValidationRule attributes on submit. If any rule fails, the form displays error messages and does not fire OnValidSubmit.
How it works
- User clicks the submit button.
DynamicFormrecursively walks the entireSchemeModeltree — including nested objects, array items, and dictionary values.- For each property/item, it collects all attributes that inherit from
ValidationRule. - Each rule's
IsValid(SchemeModel, value)is called with the current field value. - Failed rules produce error messages via
FormatErrorMessage(displayName). - Errors are displayed in a red alert box above the submit button.
Recursive validation scope
| Structure | What gets validated |
|---|---|
| Object properties | Each property is validated, then its children are recursed into. |
| Array items | Each item in the list is validated against the array's Indices schema. Nested objects inside items are recursed. |
| Dictionary values | Each value entry is validated against the dictionary's value schema. Nested objects are recursed. |
Built-in rules
RequiredRule— fails if value is null, empty string, empty list (IList), or empty dictionary.
Creating a custom validation rule
public class MinLengthRule : ValidationRule
{
public int MinLength { get; set; }
public MinLengthRule(int minLength) => MinLength = minLength;
public override bool IsValid(SchemeModel map, object? value)
{
return value is string s && s.Length >= MinLength;
}
public override string FormatErrorMessage(string name)
{
return $"{name} must be at least {MinLength} characters.";
}
}
Usage:
[RequiredRule, MinLengthRule(3)]
public string Username { get; set; }
OnDataChanged — live edit events
The OnDataChanged callback fires whenever any field in the form is edited. It is debounced by 300ms to avoid excessive calls during rapid typing.
Usage
<DynamicForm Scheme="@_scheme" Data="_formData"
OnValidSubmit="@OnSubmit"
OnDataChanged="@OnEdited">
<SubmitTemplate>
<button type="submit" class="btn btn-primary">Save</button>
</SubmitTemplate>
</DynamicForm>
@code {
private async Task OnEdited(IDictionary<string, object> data)
{
// Auto-save to server
await Http.PostAsJsonAsync("/api/config/draft", data);
}
private async Task OnSubmit(IDictionary<string, object>? data)
{
// Final save
await Http.PostAsJsonAsync("/api/config", data);
}
}
Behavior
- Debounced — waits 300ms after the last change before firing; rapid edits are coalesced.
- Full data — the callback receives the complete
Datadictionary, not just the changed field. - No validation —
OnDataChangedfires regardless of validation state; use it for drafts/auto-save. UseOnValidSubmitfor validated final saves. - Optional — if
OnDataChangedis not bound, no timer is created (zero overhead).
Data Provider — remote options
The DataProviderAttribute renders a dropdown whose options are fetched from an HTTP endpoint at runtime. The component uses HttpClient to GET the specified path and parses the JSON response as a list of key-value objects.
Attribute properties
| Property | Type | Default | Description |
|---|---|---|---|
Path | string | — | The HTTP path to fetch (relative to HttpClient.BaseAddress or absolute URL). |
ValueField | string | "value" | The JSON field name to use as the option value. |
LabelField | string | "label" | The JSON field name to use as the option display text. |
Usage
[DataProvider("/api/countries")]
public string Country { get; set; }
[DataProvider("/api/languages", "code", "name")]
public string Language { get; set; }
Expected API response
The endpoint must return a JSON array of objects. Each object should contain at least the fields specified by ValueField and LabelField:
[
{ "value": "it", "label": "Italian" },
{ "value": "en", "label": "English" },
{ "value": "es", "label": "Spanish" }
]
Or with custom field names ([DataProvider("/api/languages", "code", "name")]):
[
{ "code": "it", "name": "Italian" },
{ "code": "en", "name": "English" }
]
Component behavior
- Loading — shows a disabled "Loading..." dropdown while the request is in-flight.
- Error — if the request fails, shows the error message in a red-bordered dropdown.
- Caching — the component caches the result per
Pathand skips redundant requests on re-render. - Field matching — field name lookup is case-insensitive.
Requirements
An HttpClient must be registered in DI (standard in Blazor WebAssembly):
builder.Services.AddScoped(sp =>
new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
Environment picker — cascading project → env dropdown
The EnvironmentPickerAttribute renders two cascading dropdowns: the user first picks a project, then picks one of that project's environments. The selected environment's id is bound to the decorated property.
This is the right widget when a task needs the user to choose a workflow target in another environment — e.g. ScheduleReminderTask schedules a NEW execution and needs to know which env's workflow to fire. A single-dropdown [DataProvider] listing every env in the workspace would work, but a workspace with many projects and many envs per project becomes a wall of options. The two-step picker scopes the second list to a single project.
Attribute properties
No constructor parameters — the attribute is a marker. The endpoints (paths, response shape, workspace scoping) are fixed in the component so every consumer behaves identically. If you need different endpoints, use [DataProvider] instead.
Usage
[Label("Target workflow")]
[Tooltip("Pick the workflow this task should fire. Must be in the same workspace as the calling execution.")]
[EnvironmentPicker]
public Guid? TargetEnvironmentId { get; set; }
The decorated property must be Guid? — the picker writes the env id or null when the user clears the project selection.
Backing endpoints
The component issues two GETs against HttpClient.BaseAddress:
| Path | Returns |
|---|---|
/api/scheduler/projects | Projects in the current workspace as [{ value: <guid>, label: <name> }, …] |
/api/scheduler/projects/{projectId}/environments | Environments under that project, same shape |
Both endpoints must be workspace-scoped via the X-Workspace-Id header — the picker has no client-side firewall against listing other workspaces' projects.
Expected response
Both endpoints return a JSON array of objects with value (Guid) and label (string):
[
{ "value": "8c1d6a5e-…", "label": "Customer Onboarding" },
{ "value": "f3e2b1a0-…", "label": "Billing Cron" }
]
Component behavior
- Initial load — projects dropdown fetches
/api/scheduler/projectsand shows"Loading projects…"until it resolves. - Project selected — environments dropdown becomes enabled, fetches
/api/scheduler/projects/{id}/environments, shows"Loading environments…"until it resolves. - Project cleared — bound value resets to empty (an env from a different project would be a stale ref).
- Editing a saved row — the bound property comes back with a Guid but the project picker starts blank. The user has to reselect a project to see the matching env. Trade-off: minor UX papercut on edit, vs. an extra round-trip on every load to resolve the env's parent project.
- Timeouts / errors — each fetch has a 10-second timeout; failures show an inline error in the relevant dropdown.
Trade-offs vs. [DataProvider]
[EnvironmentPicker] | [DataProvider("/api/envs")] | |
|---|---|---|
| Number of dropdowns | 2 (project → env) | 1 |
| Endpoints required | 2 | 1 |
| Lookup performance with N projects × M envs | Per-project fetch (~M items) | Single fetch (~N×M items) |
| Re-edit UX | User reselects project | Picker remembers env directly |
| Cross-server reusability | Hard-coded paths | Configurable per attribute |
Use [EnvironmentPicker] when the workspace has many envs and grouping by project helps the user navigate. Use [DataProvider] when the list is small enough to fit in one flat dropdown or when the data isn't env-shaped.
Tooltip attribute
The TooltipAttribute adds a native browser tooltip (hover text) to a field's label. This is the simplest way to provide a short hint to the user.
Usage
[Tooltip("Maximum retry attempts before the task gives up")]
public int MaxRetries { get; set; }
[Tooltip("Comma-separated list of email addresses")]
public string Recipients { get; set; }
The tooltip text appears when the user hovers over the field label. It uses the HTML title attribute — no JavaScript or extra components required.
Documentation attribute — inline documentation
The DocumentationAttribute attaches a documentation panel to any form field. A small book icon appears next to the field's label. Clicking it opens a floating card with a description, an optional code example, and an optional link to the full documentation page.
Attribute properties
| Property | Type | Default | Description |
|---|---|---|---|
Description | string | — | Plain-text explanation of the field's purpose. |
DocumentationUrl | string | — | URL to the full documentation page (opens in new tab). |
Example | string | — | Code or value example displayed in a <pre> block. |
ExampleLanguage | string | "text" | Hint for the example language (for future syntax highlighting). |
Usage
[Documentation("The variable name to evaluate in each switch branch.")]
public string Variable { get; set; }
[Documentation("Your API key from the settings dashboard.",
"https://docs.example.com/api-key")]
public string ApiKey { get; set; }
[Documentation("JavaScript expression evaluated at runtime.",
"https://docs.example.com/js-task",
"return context.variables['count'] + 1;")]
public string JSCode { get; set; }
Combining with Tooltip
Both attributes can be used together — the tooltip provides a quick hover hint while the documentation panel provides full context:
[Tooltip("Enter your JS code here"),
Documentation("JavaScript expression evaluated at runtime.",
"https://docs.example.com/js-task",
"return context.variables['count'] + 1;")]
public string JSCode { get; set; }
Panel content
The documentation panel renders in this order:
- Description — paragraph of explanatory text.
- Example — code block with the example snippet (only if
Exampleis set). - Open full documentation — button linking to
DocumentationUrl(only if URL is set).
Behavior
- The book icon renders inline next to the label, for both
TopandInlinelabel positions. - Clicking the icon toggles the documentation panel open/closed.
- The panel floats above the form content (absolute positioned,
z-index: 1050). - A close button in the panel header dismisses it.
- When
LabelPositionisNone, the icon is not shown (no label to attach to).
Full annotated example
Here is a complete example showing how annotations map to UI:
public class SwitchTask
{
[Field("Variable to evaluate", Required = true)]
[Template(ExecutionVariables.All)]
public string Variable { get; set; }
[Field("Default branch", Grid = 6, Placeholder = "ok")]
[DefaultValueForm("ok")]
public string DefaultBranch { get; set; } = "ok";
[Field("Cases")]
public List<SwitchCase> Cases { get; set; }
}
public class SwitchCase
{
[MultipleSelect(
new[] { "Equals", "Contains", "Regex", ">", "<" },
new[] { "=", "contains", "matches", ">", "<" })]
[Grid(3), Label("Op"), DefaultValueForm("Equals")]
public string Op { get; set; } = "Equals";
[Field("Value", Grid = 5, Required = true)]
[Template(ExecutionVariables.All)]
public string Value { get; set; }
[Field("Branch", Grid = 4, Required = true)]
public string Branch { get; set; }
}
This produces:
- Variable to evaluate — a full-width template-aware input that autocompletes against workflow variables declared elsewhere in the diagram.
- Default branch — a half-width string input pre-filled with
ok. - Cases — a dynamic list rendered as a compact 3-column table (Op / Value / Branch), with a single
+ Add caseaffordance. Each row is oneSwitchCase:- Op — multi-select dropdown (3-column).
- Value — template-aware input (5-column).
- Branch — string input (4-column).
Another example with diverse field types
public class Test
{
[MultipleSelect("Italian", "Mandarin", "Ananas")]
public string TTS { get; set; }
[CodeEditor("csharp")]
public string Name { get; set; }
[TextArea]
public string Message { get; set; }
[Placeholder("Enter a value here")]
public string Message2 { get; set; }
public float Limit { get; set; }
[Range(0, 100)]
public int LimitInt { get; set; }
[CodeEditor("javascript")]
public string JSCode { get; set; }
public decimal DecimalLimit { get; set; }
public Colors Colors { get; set; }
}
public enum Colors { Black, Red, Yellow }
| Property | Rendered as |
|---|---|
TTS | Multi-select dropdown (Italian, Mandarin, Ananas) |
Name | Monaco editor with C# syntax |
Message | Multi-line textarea |
Message2 | String input with placeholder |
Limit | Float numeric input |
LimitInt | Integer numeric input (range 0–100) |
JSCode | Monaco editor with JavaScript syntax |
DecimalLimit | Decimal numeric input |
Colors | Dropdown (Black, Red, Yellow) |
Extending the system
Adding a custom renderer for a PropertyType
Override the default component for any built-in type:
config.AddCustomRenderer<MyCustomStringComponent>(PropertyType.String);
Adding a custom attribute-driven renderer
- Create an attribute that extends
DynamicRendererComponent:
public class RichTextAttribute : DynamicRendererComponent
{
public string Toolbar { get; set; } = "full";
}
- Create a Razor component that extends
FormComponentBase:
@inherits BlazorDynamicForm.Core.FormComponentBase
<!-- your rich text editor markup -->
@code {
// Access SchemeProperty.Attributes to read RichTextAttribute properties
}
- Register the mapping:
config.AddCustomAttributeRenderer<RichTextAttribute, RichTextComponent>();
- Use it on a property:
[RichText(Toolbar = "minimal")]
public string Description { get; set; }
Using YAML-defined schemas (no C# class)
You can define forms entirely in YAML without a backing C# class:
string yaml = LoadFromFile("task-config.yaml");
List<Type> attrTypes = BlazorDynamicForm.Utility.DefaultComponents;
SchemeModel scheme = Scheme.GetSchemeFromYaml(attrTypes, yaml);
// Use the scheme directly with <DynamicForm>
This is how the platform renders Task configuration forms in the Process Editor — each Module's Task schema is stored as YAML and loaded at runtime.
Component hierarchy at runtime
When <DynamicForm> renders, it produces a component tree like this:
DynamicForm
└─ ObjectComponent (root object)
├─ LabelComponent
│ └─ StringComponent ← string property
├─ LabelComponent
│ └─ IntComponent ← int property
├─ BoxComponent (collapsible)
│ └─ ObjectComponent ← nested object
│ ├─ StringComponent
│ └─ BooleanComponent
└─ ListComponent ← List<T> property
├─ ObjectComponent ← item 0
│ └─ ...
└─ ObjectComponent ← item 1
└─ ...
Each component inherits from FormComponentBase and receives:
| Parameter | Description |
|---|---|
SchemeModel | The root schema (for resolving $ref pointers). |
SchemeProperty | The property definition for this field. |
PropertyName | Display name of the property. |
Value / ValueChanged | Two-way binding for the field value. |
IsFirst | Whether this is the root-level component. |
Project structure
BlazorDynamicForm/
├── BlazorDynamicForm/ # Blazor component library
│ ├── Attributes/ # Custom attributes (Grid, Label, Box, CodeEditor, etc.)
│ ├── AttributesComponents/ # Wrapper components (BoxComponent, LabelComponent)
│ ├── Components/ # Field renderers (StringComponent, ObjectComponent, etc.)
│ ├── Core/
│ │ ├── DynamicFormConfiguration.cs # Renderer registry
│ │ └── FormComponentBase.cs # Base class for all field components
│ └── Utility.cs # DI registration (AddBlazorDynamicForm)
├── TypeAnnotationParser/ # Schema generation library
│ ├── TypeAnnotationParser.cs # Main parser (reflection-based)
│ ├── SchemeModel.cs # Root schema with References
│ ├── SchemeProperty.cs # Property node + AttributeScheme base
│ ├── PropertyType.cs # Type enum
│ ├── ParserConfiguration.cs # Parser config
│ ├── Generator/
│ │ ├── ObjectGenerator.cs # Data creation/validation from schema
│ │ └── ObjectGeneratorOptions.cs
│ └── Serialization/
│ └── Scheme.cs # YAML serialize/deserialize
├── BlazorDynamicFormTest/ # Test Blazor WASM app
└── TypeAnnotationParser.Test/ # Unit tests