Substantive.Blazor.DynamicForm 9.4.0-build20260625r1
Substantive.Blazor.DynamicForm
A Blazor library that renders nested object graphs as editable forms using FormCraft and MudBlazor.
Features
- Builds a deterministic
FormNodetree from a model instance using reflection andDisplayAttributemetadata. - Renders nested sections (root fields and child nodes) as independent FormCraft forms.
- Four layout modes:
None(flattened inline),Tabs,Stepper, andSidebar. - Attribute-driven field configuration via
AttributeFormBuilderExtensions.AddFieldsFromAttributesAndDisplayAttribute<TModel>(). DefaultFormConfigurationProvidercreates and caches FormCraft configurations per runtime type to avoid repeated reflection.- Register a custom
IFormConfigurationProvidervia dependency injection to override the default configuration strategy.
Requirements
- .NET 10
- FormCraft and MudBlazor packages available in the consuming project.
Quick start
- Add a project reference to this package or include the project in your solution.
- Ensure
FormCraftandMudBlazorare registered in your application. - Use
NestedFormCraftComponentin a Razor page or component:
@using Substantive.Blazor.DynamicForm
<NestedFormCraftComponent TModel="MyModel"
Model="@model"
Layout="GroupLayout.Tabs"
ShowSubmitButton="true"
OnValidSubmit="HandleSubmit" />
@code {
private MyModel model = new();
private Task HandleSubmit(MyModel? m)
{
// called when any nested section submits successfully
return Task.CompletedTask;
}
}
Key component parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
Model |
TModel? |
— | Model instance used to build and populate the form tree. |
MaxDepth |
int |
3 |
Maximum nested discovery depth (clamped to 0–3). |
Layout |
GroupLayout |
None |
Controls how top-level sections are presented. |
TopLevelGroupTitle |
string? |
— | Presentation-only title override for the synthetic root-fields section ("General" by default). |
TopLevelGroupDescription |
string? |
— | Presentation-only description override for the root-fields section. |
HeadingTypo |
MudBlazor.Typo |
h3 |
Base heading typography; nested headings scale relative to this value. |
ShowSubmitButton |
bool |
false |
When true, nested forms render a submit button. |
SubmitButtonText |
string? |
— | Override text for the submit button in nested forms. |
OnValidSubmit |
EventCallback<TModel?> |
— | Centralized callback invoked when any nested form validates and submits. |
Layout modes
| Value | Description |
|---|---|
GroupLayout.None |
Sections rendered inline in order (root fields first, then children ordered by Order then Title). |
GroupLayout.Tabs |
Top-level sections rendered as horizontal MudTabs. |
GroupLayout.Stepper |
Top-level sections rendered as sequential MudStepper steps. |
GroupLayout.Sidebar |
Top-level sections listed in a left sidebar; selected section content rendered to the right. Automatically switches to a dialog-based mobile navigation on smaller breakpoints. |
Field configuration with AddFieldsFromAttributesAndDisplayAttribute
AttributeFormBuilderExtensions.AddFieldsFromAttributesAndDisplayAttribute<TModel>() scans a model type for field attributes and builds the FormCraft field configuration automatically. It is called internally by DefaultFormConfigurationProvider, so no manual wiring is needed unless you build a custom provider.
Supported field attributes
| Attribute | Property type | Rendered as |
|---|---|---|
[TextField] |
string |
Text input |
[EmailField] |
string |
Email input (with optional format validation) |
[TextArea] |
string |
Multi-line textarea (supports Rows, MaxLength, AutoResize) |
[NumberField] |
int, decimal, double, float, long |
Numeric input (supports Min, Max, Step) |
[DateField] |
DateTime, DateTime? |
Date input (supports MinDate, MaxDate, Format) |
[CheckboxField] |
bool |
Checkbox |
[SelectField] |
any | Select / dropdown (supports inline Options, AllowMultiple, OptionsProviderName) |
Properties without one of the above field attributes are not rendered by FormCraft, even if they have a [Display] annotation.
Label and placeholder resolution
The extension follows a deterministic fallback order to keep UI text consistent:
- Label:
DisplayAttribute.Name→ field attribute'sLabel→ property name. - Placeholder / Prompt:
DisplayAttribute.Prompt→ field attribute'sPlaceholder.
Setting [Display(Name = "...")] on a property is therefore the cleanest way to control labels without coupling the attribute to a specific field type.
Validation attributes
The following standard System.ComponentModel.DataAnnotations attributes are applied automatically when present on the property:
| Attribute | Applied to | Effect |
|---|---|---|
[Required] |
all types | Marks the field as required; uses ErrorMessage when provided. |
[MinLength(n)] |
string |
Adds a minimum-length validator. |
[MaxLength(n)] |
string |
Adds a maximum-length validator. |
[Range(min, max)] |
numeric types | Sets min/max attributes on the input. |
[RegularExpression(pattern)] |
all types | Adds a pattern attribute and a regex validator. |
Example
using System.ComponentModel.DataAnnotations;
using FormCraft;
using Substantive.Blazor.DynamicForm;
[Display(Name = "User Profile", Description = "Manage account details")]
public class UserProfileModel
{
// Label comes from DisplayAttribute.Name; prompt from DisplayAttribute.Prompt
[Display(Name = "Full Name", Prompt = "Enter your full name", Order = 1)]
[TextField]
[Required]
[MaxLength(100)]
public string? FullName { get; set; }
// Label falls back to EmailFieldAttribute.Label because no DisplayAttribute.Name is set
[EmailField(Label = "Email Address", Placeholder = "you@example.com", ValidateFormat = true)]
[Required]
public string? Email { get; set; }
[Display(Name = "Age", Order = 3)]
[NumberField(Min = 0, Max = 150)]
[Range(0, 150, ErrorMessage = "Age must be between 0 and 150")]
public int Age { get; set; }
[Display(Name = "Bio", Order = 4)]
[TextArea(Rows = 4, MaxLength = 500)]
public string? Bio { get; set; }
[Display(Name = "Country", Order = 5)]
[SelectField(Options = ["Australia", "Canada", "United Kingdom", "United States"])]
public string? Country { get; set; }
[Display(Name = "Subscribe to newsletter", Order = 6)]
[CheckboxField(Text = "Yes, send me updates")]
public bool Newsletter { get; set; }
// Not rendered — no field attribute present
public string? InternalToken { get; set; }
// Explicitly hidden from the form
[Display(AutoGenerateField = false)]
public string? AuditTrail { get; set; }
// Complex property → becomes a child section (no field attribute needed)
[Display(Name = "Address", Description = "Postal address details", Order = 7)]
public AddressModel Address { get; set; } = new();
}
public class AddressModel
{
[Display(Name = "Street", Order = 1)]
[TextField]
public string? Street { get; set; }
[Display(Name = "City", Order = 2)]
[TextField]
public string? City { get; set; }
[Display(Name = "Postal Code", Order = 3)]
[TextField]
[RegularExpression(@"^\d{4,10}$", ErrorMessage = "Enter a valid postal code")]
public string? PostalCode { get; set; }
}
With Layout="GroupLayout.Tabs" this model produces two tabs — User Profile and Address — each rendered as an independent FormCraft form.
Configuration
Default (attribute-driven)
DefaultFormConfigurationProvider is used automatically when no IFormConfigurationProvider is registered in DI. It calls AttributeFormBuilderExtensions.AddFieldsFromAttributesAndDisplayAttribute<TModel>() to build a FormCraft configuration from your model's attributes and caches the result per type.
Custom provider via DI
Register an implementation of IFormConfigurationProvider in your service collection to control field layout, validation, and rendering for any model type:
builder.Services.AddSingleton<IFormConfigurationProvider, MyFormConfigurationProvider>();
NestedFormCraftComponent resolves the provider via [Inject] and falls back to DefaultFormConfigurationProvider when none is registered.
Model annotation with DisplayAttribute
DisplayAttribute is the primary way to control how the tree is built and how sections and fields appear in the UI.
| Property | Effect |
|---|---|
Name |
Sets the section title (for complex properties) or field label. Falls back to the property/type name when absent. |
Description |
Shown as a subtitle under the section heading or field hint. |
Order |
Controls the render order of sections and fields. Lower values appear first. Without Order, sort order is non-deterministic across runtimes. |
AutoGenerateField = false |
Hides the property from the form entirely — no section or field is generated. |
AutoGenerateField = trueis never required.NestedFormTreeBuilderincludes a property when[Display]is absent or whenAutoGenerateFieldis not explicitly set —GetAutoGenerateField()returnsnullin that case, which is treated as included. OnlyAutoGenerateField = falsechanges behaviour.
The DisplayAttribute on the root model type itself sets the title of the root section ("General" by default).
Example
using System.ComponentModel.DataAnnotations;
[Display(Name = "User Profile", Description = "Manage the user's account details")]
public class UserProfileModel
{
[Display(Name = "Full Name", Order = 1)]
public string? FullName { get; set; }
[Display(Name = "Email Address", Order = 2)]
public string? Email { get; set; }
// Hidden from the form
[Display(AutoGenerateField = false)]
public string? InternalId { get; set; }
// Complex property → becomes a child section titled "Address"
[Display(Name = "Address", Description = "Postal address details", Order = 3)]
public AddressModel Address { get; set; } = new();
}
public class AddressModel
{
[Display(Name = "Street", Order = 1)]
public string? Street { get; set; }
[Display(Name = "City", Order = 2)]
public string? City { get; set; }
[Display(Name = "Postal Code", Order = 3)]
public string? PostalCode { get; set; }
}
With the model above and Layout="GroupLayout.Tabs", the component renders two tabs — User Profile (root fields: Full Name, Email) and Address (Address child section) — in that order.
How it works
NestedFormTreeBuilderinspects the model type using reflection. Complex navigation properties become childFormNodeentries; primitive or displayable properties at the root are collected into a syntheticRootFieldsNodetitled "General".NestedFormCraftComponentbuilds the tree onOnParametersSet, resolves a FormCraft configuration for each node type, and delegates rendering to either the flatNestedFormRenderer(forGroupLayout.None) or a layout component.- Each
FormNodeowns an independentFormCraftComponentinstance. On valid submit, the typed callback forwards the current model value to the top-levelOnValidSubmitcallback.
References
- FormCraft: https://phmatray.github.io/FormCraft/home
- MudBlazor: https://mudblazor.com
Showing the top 20 packages that depend on Substantive.Blazor.DynamicForm.
| Packages | Downloads |
|---|---|
|
Substantive.DevTools.Blazor
Substantive Blazor development tools.
|
9 |
|
Substantive.DevTools.Blazor
Substantive Blazor development tools.
|
10 |
|
Substantive.DevTools.Blazor
Substantive Blazor development tools.
|
11 |
|
Substantive.DevTools.Blazor
Substantive Blazor development tools.
|
12 |
|
Substantive.DevTools.Blazor
Substantive Blazor development tools.
|
21 |
|
Substantive.DevTools.Blazor
Substantive Blazor development tools.
|
24 |
|
Substantive.Metapackage.Blazor
A metapackage contains all Blazor essential building blocks. It packed standalone Blazor components and extensions that often use such as JSInterop, BrowserInterop, Navigation, Exception Handling, Localization, etc,.
|
55 |
.NET 10.0
- FormCraft (>= 2.5.0)
- FormCraft.ForMudBlazor (>= 2.5.0)
- Microsoft.AspNetCore.Components.Web (>= 10.0.9)
- MudBlazor (>= 8.15.0)