A form represents a complete user interaction in UI Metadata Framework. Each form defines:
Input fields : Data the user provides
Output fields : Data returned by the server
Behavior : How the form should behave (auto-submit, modal behavior, etc.)
Metadata : Labels, IDs, and custom properties
The FormAttribute is used to decorate form classes and configure their behavior:
/home/daytona/workspace/source/UiMetadataFramework.Core/Binding/Form/FormAttribute.cs
public class FormAttribute : Attribute
{
public string ? Id { get ; set ; }
public string ? Label { get ; set ; }
public bool PostOnLoad { get ; set ; }
public bool PostOnLoadValidation { get ; set ; } = true ;
public bool CloseOnPostIfModal { get ; set ; } = true ;
}
Unique identifier for the form. If not specified, the fully-qualified type name is used.
[ Form ( Id = "user-registration" )]
public class RegisterUser : Form < Request , Response >
{
// ...
}
Using explicit IDs makes your API more stable. Type names can change during refactoring, but explicit IDs remain constant.
Label
Human-readable label displayed in the UI:
[ Form ( Label = "User Registration Form" )]
public class RegisterUser : Form < Request , Response >
{
// ...
}
PostOnLoad
When true, the form is automatically submitted when loaded. Perfect for reports and dashboards:
[ Form ( Label = "Sales Report" , PostOnLoad = true )]
public class SalesReport : Form < Request , Response >
{
// Form auto-submits on load to display data
}
Use PostOnLoad = true for read-only forms that display data without requiring user input.
PostOnLoadValidation
Controls whether validation runs during the initial PostOnLoad submission:
[ Form (
PostOnLoad = true ,
PostOnLoadValidation = false // Skip validation on initial load
)]
public class Dashboard : Form < Request , Response >
{
// ...
}
Default is true. Set to false if you want to submit with default/empty values on initial load.
CloseOnPostIfModal
Controls modal behavior when the form is displayed as a modal dialog:
[ Form (
Label = "Quick Action" ,
CloseOnPostIfModal = true // Auto-close modal after submission
)]
public class QuickAction : Form < Request , Response >
{
// ...
}
true (default): Modal closes automatically after successful submission
false: Modal remains open (useful for multi-step workflows)
A typical form consists of three parts:
Inherits from Form<TRequest, TResponse> and implements the handler:
[ Form ( Id = "Magic" , Label = "Do some magic" , PostOnLoad = false )]
public class Magic : Form < Magic . Request , Magic . Response >
{
protected override Response Handle ( Request request )
{
// Process the request
return new Response
{
FirstName = request . FirstName ,
DateOfBirth = request . DateOfBirth
};
}
}
2. Request Class
Defines input fields using InputFieldAttribute:
public class Request : IRequest < Response >
{
[ InputField ( Label = "First name" , OrderIndex = 1 , Required = true )]
public string FirstName { get ; set ; }
[ InputField ( Label = "DoB" , OrderIndex = 2 , Required = true )]
public DateTime DateOfBirth { get ; set ; }
[ InputField ( Hidden = true )]
public int Height { get ; set ; }
}
3. Response Class
Defines output fields using OutputFieldAttribute:
public class Response : FormResponse < FormResponseMetadata >
{
[ OutputField ( Label = "First name" , OrderIndex = 1 )]
public string ? FirstName { get ; set ; }
[ OutputField ( Label = "DoB" , OrderIndex = 2 )]
public DateTime ? DateOfBirth { get ; set ; }
[ OutputField ( Hidden = true )]
public int Height { get ; set ; }
}
You can extend FormAttribute to add custom metadata:
public class MyFormAttribute : FormAttribute
{
public override IDictionary < string , object ?> GetCustomProperties ( Type type )
{
var menuAttribute = type . GetTypeInfo ()
. GetCustomAttribute < MenuAttribute >();
return new Dictionary < string , object ?>
{
{ "ParentMenu" , menuAttribute ? . ParentMenu }
};
}
}
public class MenuAttribute : Attribute
{
public MenuAttribute ( string parentMenu )
{
this . ParentMenu = parentMenu ;
}
public string ParentMenu { get ; set ; }
}
[ MyForm ( Label = "User Management" )]
[ Menu ( "Admin" )]
public class UserManagement : Form < Request , Response >
{
// The generated metadata will include:
// { "ParentMenu": "Admin" }
}
Custom properties are serialized to JSON and sent to the client, allowing you to extend the framework with application-specific metadata.
Forms must be registered with the FormRegister:
var binder = new MetadataBinder ();
binder . RegisterAssembly ( typeof ( StringInputComponentBinding ). Assembly );
var formRegister = new FormRegister ( binder );
formRegister . RegisterForm ( typeof ( Magic ));
// Get form metadata
var formInfo = formRegister . GetFormInfo ( typeof ( Magic ));
var metadata = formInfo . Metadata ;
// Access metadata properties
Console . WriteLine ( metadata . Id ); // "Magic"
Console . WriteLine ( metadata . Label ); // "Do some magic"
Console . WriteLine ( metadata . PostOnLoad ); // false
Console . WriteLine ( metadata . InputFields . Count ); // 5
Console . WriteLine ( metadata . OutputFields . Count ); // 4
The framework resolves form IDs in this order:
Explicit Id property in FormAttribute
Fully-qualified type name
// Explicit ID
[ Form ( Id = "custom-id" )]
public class MyForm { } // ID: "custom-id"
// Derived from type name
[ Form ]
public class MyForm { } // ID: "Namespace.MyForm"
Form IDs must be unique within your application. Attempting to register two forms with the same ID will throw an InvalidConfigurationException.
Complete Example
Here’s a complete form from the test suite:
/home/daytona/workspace/source/UiMetadataFramework.Tests/MediatrTests.cs:87-92
[ MyForm ( Id = "Magic" , Label = "Do some magic" , PostOnLoad = false , CloseOnPostIfModal = true )]
[ Menu ( "Magical tools" )]
public class Magic : BaseForm
{
protected override Response Handle ( Request request )
{
return new Response
{
FirstName = request . FirstName ,
DateOfBirth = request . DateOfBirth
};
}
public class Request : IRequest < Response >
{
[ InputField ( Label = "First name" , OrderIndex = 1 , Required = true )]
public string FirstName { get ; set ; }
[ InputField ( Label = "DoB" , OrderIndex = 2 , Required = true )]
public DateTime DateOfBirth { get ; set ; }
[ InputField ( Hidden = true )]
public int Height { get ; set ; }
[ InputField ( Hidden = true )]
public decimal Weight { get ; set ; }
}
public class Response : FormResponse < FormResponseMetadata >
{
[ OutputField ( Label = "First name" , OrderIndex = 1 )]
public string ? FirstName { get ; set ; }
[ OutputField ( Label = "DoB" , OrderIndex = 2 )]
public DateTime ? DateOfBirth { get ; set ; }
[ OutputField ( Hidden = true )]
public int Height { get ; set ; }
[ OutputField ( Hidden = true )]
public decimal Weight { get ; set ; }
}
}
Forms support event handlers that run at specific points in the form lifecycle:
public class LogFormEvent : Attribute , IFormEventHandlerAttribute
{
public LogFormEvent ( string eventName )
{
this . RunAt = eventName ;
}
public string Id { get ; } = "log-form-event" ;
public string RunAt { get ; }
public EventHandlerMetadata ToMetadata ( Type formType , MetadataBinder binder )
{
return new EventHandlerMetadata ( this . Id , this . RunAt );
}
}
[ Form ( Label = "My Form" )]
[ LogFormEvent ( FormEvents . FormLoaded )]
public class MyForm : Form < Request , Response >
{
// Event handler runs when form loads
}
Next Steps
Input Fields Learn how to define and validate input fields
Output Fields Understand how to structure response data
Metadata Binding See how forms are converted to metadata
Quick Start Build your first form