WebForms Core is a modern, server-orchestrated technology for building interactive HTML user interfaces, introduced by Elanat in 2024. It enables direct and deterministic DOM management from the server, eliminating the need for separate client-side frameworks such as React, Vue, or Angular.
WebForms Core 2.2 has been released.
This release introduces several new capabilities, but one feature deserves special attention: Render.
Render is a high-level UI rendering primitive designed to make repeated server-side rendering of an existing HTML element predictable and manageable. It brings together several mechanisms that are all new in this version—Snapshot, Rollback, and Transient DOM—into a single high-level primitive.
The result is a simpler way to replace, rebuild, and restore parts of an HTML document while keeping the operation under server-side control.
Render
In this new version, Snapshot, Rollback, and Render are all new additions. Render is a high-level method that uses Snapshot and Rollback internally.
In WebForms Core 2.2, Render brings these operations together.
A simple example is:
form.Render(f =>
{
f.Replace(InputPlace.Root, "{{name}}", name);
f.Replace("-", "{{email}}", email);
f.Replace("-", "{{phone}}", phone);
f.Replace("-", "{{age}}", age);
f.Replace("-", "{{city}}", city);
f.Replace("-", "{{country}}", country);
}, "user");
The rendering process can also be used directly with a template:
var data = new Dictionary<string, string>
{
{ "name", name},
{ "email", email},
{ "phone", phone},
{ "age", age},
{ "city", city},
{ "country", country}
};
string json = JsonSerializer.Serialize(data);
form.Render(f =>
{
f.BindJSONToTemplate(InputPlace.Root, json, "", "{{value}}");
}, "user");
The important part is not simply the number of lines saved by Render. The important part is that the lifecycle of the rendered element is handled as one operation.
Internally, Render checks whether a previous version of the selected element already exists. If it does, that version is restored. Otherwise, a Snapshot is created.
The rendering process then enters the Transient DOM, appends the generated WebForms commands, exits the Transient DOM, and appends the new form.
In simplified form:
Existing State
↓
Rollback or Snapshot
↓
Start Transient DOM
↓
Apply New Rendering
↓
End Transient DOM
↓
New UI State
This is particularly useful when the same element has to be rendered repeatedly with different data or templates.
Render and Transient DOM
Render uses the Transient DOM internally.
This is important when selecting InputPlace values inside the Render operation. The root of the selected InputPlace must be considered relative to the Transient DOM being processed.
Render is therefore not simply a shortcut for a sequence of unrelated commands. It creates a controlled rendering process in which the existing state and the new state can be handled together.
The implementation is:
// The Render Method is Sensitive to DOM Changes; It is Recommended to Assign a Stable ID to the Selected Element.
// This Method Utilizes the Transient DOM; Therefore, When Selecting InputPlaces, You Must Consider the Root of the Selected InputPlace Within the Method.
public WebForms Render(WebForms newForm, string InputPlace, string Key = "", bool Permanent = false)
{
if (newForm == null)
return this;
string bodyData = newForm.GetWebFormsData();
if (string.IsNullOrEmpty(bodyData))
return this;
if (string.IsNullOrEmpty(Key))
Key = InputPlace;
WebForms form = new WebForms();
form.Exist(Permanent ? Fetch.Cache(Key) : Fetch.Save(Key));
form.Rollback(InputPlace, Key, Permanent);
form.Else();
form.Snapshot(InputPlace, Key, Permanent);
form.StartTransientDOM(InputPlace);
AppendForm(form);
newForm.EndTransientDOM();
AppendForm(newForm);
return this;
}
There is also a configuration-based overload:
public WebForms Render(
System.Action<WebForms> configure,
string InputPlace,
string Key = "",
bool Permanent = false)
{
var newForm = new WebForms();
configure(newForm);
return Render(newForm, InputPlace, Key, Permanent);
}
This makes Render suitable for both manually constructed WebForms and dynamically configured rendering operations.
Snapshot and Rollback
Render is built on top of the Snapshot and Rollback mechanisms introduced in WebForms Core 2.2.
A Snapshot stores the state of a selected tag so that it can later be restored.
Consider a simple multilingual user template.
The page contains three buttons:
<input id="English" type="button" value="en-user">
<input id="Spanish" type="button" value="es-user">
<input id="Persian" type="button" value="fa-user">
<div id="user">
</div>
The templates are defined using HTML <template> elements:
<template id="en-user">
<div id="user">
<field>Name: {{name}}</field>
<a href="mailto:{{email}}">Email: {{email}}</a>
<field>Phone: {{phone}}</field>
<field>Age: {{age}}</field>
<field>City: {{city}}</field>
<field>Country: {{country}}</field>
</div>
</template>
<template id="es-user">
<div id="user">
<field>Nombre: {{name}}</field>
<a href="mailto:{{email}}">Correo electrónico: {{email}}</a>
<field>Teléfono: {{phone}}</field>
<field>Edad: {{age}}</field>
<field>Ciudad: {{city}}</field>
<field>País: {{country}}</field>
</div>
</template>
<template id="fa-user">
<div id="user">
<field>نام: {{name}}</field>
<a href="mailto:{{email}}">ایمیل: {{email}}</a>
<field>تلفن: {{phone}}</field>
<field>سن: {{age}}</field>
<field>شهر: {{city}}</field>
<field>کشور: {{country}}</field>
</div>
</template>
The WebForms code creates a Snapshot for each template:
using CodeBehind;
public partial class SnapshotAndRollbackController : CodeBehindController
{
public void PageLoad(HttpContext context)
{
WebForms form = new WebForms();
form.Snapshot("en-user");
form.Snapshot("es-user");
form.Snapshot("fa-user");
form.SetCommentEvent(
InputPlace.AllAttributes("type", "button"),
HtmlEvent.OnClick,
"set-state");
form.StartIndex("set-state");
form.Rollback("user", Fetch.GetValue("$"));
form.Replace("user", "{{name}}", Fetch.GetValue("name"));
form.Replace("user", "{{email}}", Fetch.GetValue("email"));
form.Replace("user", "{{phone}}", Fetch.GetValue("phone"));
form.Replace("user", "{{age}}", Fetch.GetValue("age"));
form.Replace("user", "{{city}}", Fetch.GetValue("city"));
form.Replace("user", "{{country}}", Fetch.GetValue("country"));
Write(form.ExportToHtmlComment());
}
}
When one of the buttons is clicked, the corresponding Snapshot is restored and the user data is inserted into the selected template.
The API itself is intentionally small:
public void Snapshot(
string InputPlace,
string Key = "",
bool Permanent = false)
public void Rollback(
string InputPlace,
string Key = "",
bool Permanent = false)
The Snapshot and Rollback mechanism is sensitive to DOM changes, so assigning a stable ID to the selected element is recommended.
Render uses these mechanisms automatically, so developers do not need to manually coordinate Snapshot and Rollback when using the high-level rendering API.
GetTagHash
WebForms Core 2.2 also introduces Fetch.GetTagHash.
It calculates a hash from the outerHTML of a tag.
form.SetCommentEvent(
"GetHash",
HtmlEvent.OnClick,
"get-hash");
form.StartIndex("get-hash");
form.AddText("Text", "-a");
form.Message(Fetch.GetTagHash("Box"));
The hash is based only on the tag's outerHTML.
This makes it useful for detecting whether the DOM representation of a tag has changed.
Formatting Values with Regex
WebForms Core 2.2 adds:
SetFormatCacheValueSetFormatSaveValue
These methods apply Regex-based formatting to values stored in Cache or Save.
For example, the following page contains a money value and a time value:
<button id="SetFormat">Click to Set Format</button>
<input type="text" id="Money" value="12345678">
<input type="text" id="Clock" value="15:1:3">
The WebForms code stores each value and applies a regular-expression replacement:
using CodeBehind;
public partial class SetFormatController : CodeBehindController
{
public void PageLoad(HttpContext context)
{
WebForms form = new WebForms();
form.SetCommentEvent(
"SetFormat",
HtmlEvent.OnClick,
"set-format");
form.StartIndex("set-format");
form.SaveValue("Money", "money");
form.SetFormatSaveValue(
"money",
"(\\d)(?=(\\d{3})+$)",
"$$1,");
form.SetValue("Money", Fetch.Save("money"));
form.CacheValue("Clock", "clock");
form.SetFormatCacheValue(
"clock",
"(^|:)(\\d)(?=:|$)",
"$$10$2"); // Two $$ for escape JavaScript interpretation
form.SetValue("Clock", Fetch.Cache("clock"));
Write(form.ExportToHtmlComment());
}
}
The first operation formats:
12345678
as:
12,345,678
The second transforms:
15:1:3
into:
15:01:03
The $$ in the replacement string is important because it escapes the $ for JavaScript interpretation.
This is also useful when formatting must happen as part of a larger WebForms operation, where the formatted value can then be applied back to the corresponding HTML element.
Arithmetic Operations
WebForms Core 2.2 adds arithmetic operations for values stored in Cache and Save.
The supported operations are:
+
-
*
/
%
//
**
The following example provides a button for each operation:
<button id="Addition">+</button>
<button id="Subtraction">-</button>
<button id="Multiplication">*</button>
<button id="Division">/</button>
<button id="Remainder">%</button>
<button id="FloorDivision">//</button>
<button id="Power">**</button>
<input type="text" id="Value1" value="10"> |
<input type="text" id="Value2" value="7">
=
<b id="Result"></b>
The WebForms code is:
using CodeBehind;
public partial class ArithmeticController : CodeBehindController
{
public void PageLoad(HttpContext context)
{
WebForms form = new WebForms();
form.SetCommentEvent(
"<button>*",
HtmlEvent.OnClick,
"result");
form.StartIndex("result");
form.SaveValue("Value1", "result");
form.SetArithmeticSaveValue(
"result",
Fetch.GetText("$"),
Fetch.GetValue("Value2"));
form.SetText(
"Result",
Fetch.Save("result"));
Write(form.ExportToHtmlComment());
}
}
The // operation is implemented in WebFormsJS as:
case "//":
return Math.floor(a / b);
For example, with 10 and 7, / produces the normal division result, while // produces the floored result.
The operations allow common numerical transformations to be performed directly within the WebForms command flow.
Text Operations
WebForms Core 2.2 currently provides six text operations:
textafter
textafterlast
textbefore
textbeforelast
substring
remove
The example below provides a button for each operation:
<button>textafter</button>
<button>textafterlast</button>
<button>textbefore</button>
<button>textbeforelast</button>
<button>substring</button>
<button>remove</button>
<input type="text" id="Value1" value="This is a Hello World!"> |
<input type="text" id="Value2" value=""> |
<input type="text" id="Value3" value="">
=
<b id="Result"></b>
The WebForms implementation is:
using CodeBehind;
public partial class TextOperationController : CodeBehindController
{
public void PageLoad(HttpContext context)
{
WebForms form = new WebForms();
form.SetCommentEvent(
"<button>*",
HtmlEvent.OnClick,
"result");
form.StartIndex("result");
form.SaveValue("Value1", "result");
form.SetTextOperationSaveValue(
"result",
Fetch.GetText("$"),
Fetch.GetValue("Value2"),
Fetch.GetValue("Value3"));
form.SetText(
"Result",
Fetch.Save("result"));
Write(form.ExportToHtmlComment());
}
}
The current set is intentionally compact. More text operations can be added in future versions based on developer feedback and practical use.
Regex-Based Element Selection
The Criteria system can use regular expressions to select elements.
Consider the following HTML:
<div id="Regex">
<div>2026-10-01</div>
<div>Adrian</div>
<div>43</div>
<div>00125478744</div>
<div>Brazil</div>
</div>
The following commands select elements based on their text:
form.SetBackgroundColor(
"Regex|*?t#\\d{4}-\\d{2}-\\d{2}$",
"pink");
form.SetBackgroundColor(
"Regex|*?t#^[0-9]+$",
"brown");
The t criterion works with the text of the element, while # specifies a regular-expression comparison.
The first expression matches a date such as:
2026-10-01
The second matches text consisting entirely of digits, such as:
43
00125478744
The underlying WebFormsJS implementation uses JavaScript's RegExp:
case '#':
{
let pattern = tmpValue;
let flags = "";
const m = pattern.match(/^\/([\s\S]+)\/([a-z]*)$/);
if (m)
{
pattern = m[1];
flags = m[2];
}
try
{
result = new RegExp(pattern, flags).test(text);
}
catch
{
result = false;
}
break;
}
This allows the Criteria system to perform more precise element selection without requiring a separate selector language.
JavaScript Objects from WebForms
A value beginning with $ in a WebForms class method can be interpreted by WebFormsJS as a JavaScript object reference.
This provides a direct way for server-generated commands to refer to objects that exist in the browser.
The distinction is important: the WebForms class generates the command, while WebFormsJS performs the corresponding client-side execution.
Try and Catch
WebForms Core 2.2 adds Try and Catch for explicit error handling.
For example:
form.Try();
form.AddTag("<h5>", "div");
form.SetText("<h5>|<>-1", "New tag!");
form.Message("New Tag Was Added!");
form.Catch();
form.Message("h5 Tag Not Exist 1!");
form.Message("h5 Tag Not Exist 2!");
If an Action Control inside Try encounters an error, execution stops and continues at Catch.
Without Try and Catch, there is no automatic recovery mechanism. The developer decides which operations may cause problems and places those operations inside an appropriate error-handling structure.
LockQueue
The queue behavior was changed in WebForms Core 2.2.
In the previous version, the queue was automatically locked. In 2.2, the queue is no longer automatically locked in that way.
The default duration is controlled by:
WebFormsOptions.QueueLockDuration = 100;
When a process needs additional protection, the developer can explicitly call:
form.LockQueue(10000);
LockQueue dynamically increases the lock duration for the current process and then restores the previous configured value automatically.
This is useful when a process must be allowed to complete before another queued process starts doing something else.
Native HTML Validation from the Server
WebForms Core 2.2 adds SetCustomValidity.
WebForms Core does not introduce a new validation framework.
Instead, it brings the browser's existing validation capability into the server-generated command model.
For example:
form.SetCommentEvent(
"(name)",
HtmlEvent.OnInput,
"custom-validity");
form.StartIndex("custom-validity");
form.IsEqualTo("admin", Fetch.GetValue("$"));
form.SetCustomValidity(
"$",
"The username cannot be \"admin\".");
form.Else();
form.SetCustomValidity("$", "");
The browser's native Constraint Validation API remains responsible for the validation behavior.
The server determines the condition and sends the appropriate command to the HTML element.
Stable WebAssembly Support
WebForms Core 2.2 brings stable WebAssembly support across seven languages and environments:
- AssemblyScript
- C
- C++
- C#
- Go
- Java
- Rust
Go and Rust provide WebAssembly targets directly, while AssemblyScript is designed specifically for WebAssembly.
For example, Go can be built with its native WebAssembly target:
@echo off
set GOOS=js
set GOARCH=wasm
go build -o webforms-go.wasm
for /f "delims=" %%i in ('go env GOROOT') do set GOROOT=%%i
copy "%GOROOT%\lib\wasm\wasm_exec.js" wasm_exec.js
pause
Rust can be built with the WebAssembly target and processed with wasm-bindgen:
cargo +stable-x86_64-pc-windows-gnu build --release --target wasm32-unknown-unknown
wasm-bindgen target/wasm32-unknown-unknown/release/webformscore_wasm_test.wasm --target web --out-dir pkg
The current implementations are:
- AssemblyScript: WebAssembly
- C: Emscripten
- C++: Emscripten
- C#: AOT and Runtime
- Go: native WebAssembly support
- Java: TeaVM
- Rust: native WebAssembly support and wasm-bindgen
WebAssembly does not change the WebForms Core command model. It provides another execution environment while WebForms Core continues to generate WebForms responses and commands.
WebForms Classes Across 18 Programming Languages
WebForms Classes are now directly available for 18 programming languages, making WebForms Core accessible across a broad range of programming environments.
The collection includes AssemblyScript, C, C++, C#, Dart, Elixir, Go, Java, JavaScript, Julia, Perl, PHP, Python, R, Ruby, Rust, Swift, and TypeScript.
WebForms Core 2.2 is currently available for .NET and Java. The remaining language implementations will be updated to version 2.2 over the next 10 days.
This means the WebForms Classes ecosystem is not limited to a single backend language. Developers can use the same WebForms Core architecture and programming model across a wide range of languages and environments, while each implementation remains native to its own ecosystem.
Why WebForms Core Did Not Add Middleware
One of the hardest architectural decisions during the development of 2.2 was whether to introduce a generic Middleware structure into WebForms Core.
Technically, it was possible to build a very general Function-level Middleware system.
For example:
function A(x, y) {
const result = Middleware.call(this, A, B, arguments);
if (result !== undefined)
return result;
return x * y;
}
function B(x, y) {
return [2, 20];
}
function Middleware(method, middleware, args) {
if (!(this.__middleware ??= []).includes(method.name)) {
this.__middleware.push(method.name);
const newArgs = middleware.apply(this, args);
const result = method.apply(this, newArgs);
this.__middleware.splice(
this.__middleware.indexOf(method.name),
1);
return result;
}
}
Such a system could do much more than a conventional Middleware implementation. It could change arguments, stop execution, and execute the original function again with new arguments.
But there was a problem.
Adding this abstraction to WebFormsJS would make the core considerably more complicated. Lifecycle management, recursion, execution order, argument changes, and the different paths through which Functions are executed would all have to be managed.
In the end, we decided not to add it.
Instead, when a requirement is genuinely specific to client-side processing, a JavaScript Module is a better place for that logic.
For example, if form data needs to be reduced or modified before submission, a JavaScript Module can perform that operation directly without adding a generic Middleware layer to WebFormsJS.
This decision is not a statement that Middleware is impossible in WebForms Core.
It is an architectural decision to keep the core smaller and move client-specific behavior into JavaScript Modules.
Other Changes
WebForms Core 2.2 also includes several smaller improvements and fixes.
Reflection renamed to Reflect
The Reflection method has been renamed to:
Reflect
Debugger numbering
Debugger logs now include numbering, making it easier to follow the execution sequence of generated Action Controls.
JavaScript Object escaping
An escaping issue involving JavaScript Objects containing two consecutive $ characters was fixed.
WebForms Core 2.1.1 and 2.1.2
The 2.1.x maintenance releases also included several changes that lead into the 2.2 release:
- Fixed the hash-link behavior so hash links no longer create an unnecessary server request.
- Added the Modulo Filter to Criteria.
- Fixed Replace behavior for values stored in Save and Cache.
- Added Fetch support in InputPlace.
- Removed CSharpMediator.
- CSharpWASM can infer the JavaScript file path from the WASM file path.
- CSharpWASM supports both CLI and AOT.
- Rust WASM can be built through wasm-bindgen or directly as WebAssembly.
Final Thoughts
WebForms Core 2.2 is not simply a collection of additional methods.
The most important change is the introduction of higher-level operations that allow developers to describe what should happen to a UI without manually coordinating every underlying step.
Render is the clearest example.
Snapshot, Rollback, and Transient DOM already provided the mechanisms needed to manage complex DOM transformations. Render combines those mechanisms into a single operation that can manage the transition from an existing UI state to a new one.
At the same time, 2.2 keeps the underlying system explicit.
Native browser validation remains native browser validation. Client-specific processing can remain in JavaScript Modules. WebAssembly provides additional execution environments without changing the WebForms Core command model. Queue locking is explicit when a process actually needs it.
The result is a more capable WebForms Core without turning the core itself into a larger collection of abstractions.
Related links
On Elanat:
On GitHub:

Top comments (0)