Forms
A form is a class. Its DataAnnotations are the rules, [WebField] and friends are the presentation,
and the same definition drives the markup, the browser validation, the server validation and the
binding back to the model.
Defining a form
[WebForm(Id = "event-signup", Title = "Conference sign-up", SubmitLabel = "Sign me up")]
public sealed class EventSignupForm
{
[Required, StringLength(80)]
[WebField(Label = "Full name", Autocomplete = "name")]
public string? Name { get; set; }
[Required, WebOptions(Provider = "countries")]
public string? Country { get; set; }
[WebShowWhen(nameof(Country), "other"), StringLength(60)]
[WebField(Label = "Which country?")]
public string? OtherCountry { get; set; }
[WebOptions(Provider = "tracks")]
[WebField(WebFieldType.CheckboxList, Label = "Interesting tracks")]
public List<string> Tracks { get; set; } = [];
[WebRepeater(Min = 0, Max = 3)]
[WebField(Label = "Guests")]
public List<GuestEntry> Guests { get; set; } = [];
[WebFile(Extensions = ".png,.jpg,.webp", MaxBytes = 2 * 1024 * 1024, ImagesOnly = true, MaxWidth = 4096, MaxHeight = 4096)]
public WebUploadedFile? Photo { get; set; }
[WebField(WebFieldType.Checkbox, Label = "I accept the code of conduct")]
public bool AcceptConduct { get; set; }
}
Register it, plus any options providers and cross-field validators:
web.AddForm<EventSignupForm>()
.AddFormOptions("countries", [new WebFieldOption("dk", "Denmark"), new WebFieldOption("se", "Sweden")])
.AddFormOptions("tracks", [new WebFieldOption("web", "Web"), new WebFieldOption("security", "Security")])
.AddFormValidator<EventSignupForm, EventSignupValidator>();
Plugins and contributors use b.MapForm<T>() / b.MapFormOptions(provider); their forms are removed
with them, and a plugin cannot replace a form the application registered.
Field types: Text, TextArea, Email, Password, Number, Range, Date, Time, DateTimeLocal, Color, Tel, Url, Hidden, Checkbox, CheckboxList, Radio, Select, MultiSelect, Autocomplete, RichText, File. The type is inferred from the property type and attributes when not given.
Rules: Required, StringLength, Range, EmailAddress, Url, Compare, RegularExpression
(with a timeout), plus IWebFormValidator<T> for cross-field and async rules (e.g. "name taken").
Messages come from the localizer (IWebTextLocalizer).
Rendering
In a template: {form:event-signup} (optional action="/path", class="...", submit="...",
renderer="plain"). In code: await request.Forms.RenderAsync<EventSignupForm>(...).
The renderer writes labels, help text, ARIA attributes, native validation attributes, the CSRF field,
the anti-bot fields and a JSON schema the client uses for the same rules. forms.renderer selects
bootstrap5 (default) or plain; add your own with web.AddFormRenderer<T>(). A theme can override
one field type with forms/fields/{type}.html.
Handling the post
b.MapPage("/signup", async (r, ct) =>
{
var submission = await r.Forms.BindAsync<EventSignupForm>(ct);
if (!submission.IsValid)
return submission.Invalid(fallbackTemplate: "templates/signup"); // 422; JSON for the client
await signups.SaveAsync(submission.Model, ct);
return FormResult.Success("Thanks!", "/signup/done"); // JSON for the client, 303 otherwise
}, "POST").AllowAnonymous().WithRateLimit("forms");
- Posted by the client (
data-wl-form): errors appear next to the fields without a reload;FormResultcan carry a message, a redirect, data and client commands. - Posted without JavaScript: an invalid form re-renders
fallbackTemplatewith the values and errors; success redirects with303. submission.AddError(field, message)adds server-side errors (empty field = form-level).request.Forms.Prefill(model)fills an edit form before rendering.
Options and lookups
IWebFormOptionsProvider supplies values for selects, radios, checkbox lists and autocomplete
(GetOptionsAsync, SearchAsync, IsValidAsync). Posted values are always checked against the
provider. Providers are private by default: set AllowAnonymous or a Policy. The client calls
/_weblogic/forms/options/{provider} (rate-limited). WebFormHttpLookupProvider proxies an external
HTTPS lookup, only to hosts listed in forms.allowedLookupHosts.
Remote validation of a single field ([WebField(Remote = true)]) is served at
/_weblogic/forms/{form}/validate/{field}; fields without Remote are not reachable there.
Uploads
WebUploadedFile properties are bound from multipart posts with size, count and extension limits,
magic-byte detection (DetectedType) and image dimensions read by WebLogic's own header reader (PNG,
JPEG, GIF, WebP, BMP). Nothing is stored automatically; call an IWebUploadSink (web.UseUploadSink<T>(),
default on CL.Storage) or handle the stream yourself. Stored names are random; the original name is
only metadata.
Wizards
public sealed class ProjectWizardForm
{
[WebStep("basics", Title = "Basics", Order = 1), Required]
public string? ProjectName { get; set; }
[WebStep("team", Title = "Team", Order = 2), Range(1, 50)]
public int TeamSize { get; set; }
}
var submission = await r.Forms.BindAsync<ProjectWizardForm>(ct);
if (!submission.IsComplete || !submission.IsValid)
return submission.Continue("templates/wizard"); // next step, or the same step with errors
Earlier steps travel in a DataProtection-protected token (no server state), each post only accepts the current step's fields, and the last step validates everything again.
Anti-bot
On by default per form ([WebForm(AntiBot = false)] to turn off, e.g. for sign-in so password
managers can submit at once): a honeypot field and a protected timestamp that rejects posts faster
than forms.minSubmitSeconds or older than forms.maxFormAgeMinutes. Rejections publish
FormBotRejectedEvent.