diff --git a/README.md b/README.md
index 2f65336..d92a5e5 100644
--- a/README.md
+++ b/README.md
@@ -381,7 +381,8 @@ Antiforgery metadata is enabled by default. `` renders the curren
the companion script attaches the token to non-GET HTMX requests and refreshes it after boosted navigation.
> [!WARNING]
-> The companion script sends the token but does not perform validation. The application must still enable server-side antiforgery validation for the relevant endpoints.
+> The companion script sends the token but does not validate it. Razor Pages validates unsafe HTTP methods automatically.
+> MVC applications must enable server-side antiforgery validation for the relevant actions.
For example, MVC applications can validate all unsafe actions globally:
@@ -485,7 +486,7 @@ by HTMX 2.x and 4.x; only HTMX 1.9.x needs the extension.
## Sample
The [`samples/Ramstack.HtmxToolkit.Demo`](samples/Ramstack.HtmxToolkit.Demo) project demonstrates request detection,
-response headers, Tag Helpers, polling, boosted navigation, and antiforgery integration.
+response headers and events, MVC attributes, Tag Helpers, polling, boosted navigation, and antiforgery integration.
Run it with:
diff --git a/samples/Ramstack.HtmxToolkit.Demo/Controllers/InventoryController.cs b/samples/Ramstack.HtmxToolkit.Demo/Controllers/InventoryController.cs
new file mode 100644
index 0000000..1f8724f
--- /dev/null
+++ b/samples/Ramstack.HtmxToolkit.Demo/Controllers/InventoryController.cs
@@ -0,0 +1,18 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace Ramstack.HtmxToolkit.Demo.Controllers;
+
+public sealed class InventoryController : Controller
+{
+ [HttpGet]
+ [HtmxRequest]
+ [HtmxResponse(Reswap = HtmxSwap.OuterHtml)]
+ public IActionResult Status(string sku)
+ {
+ var content = sku == "BOOK-42"
+ ? "BOOK-42: Hypermedia Systems — 3 copies in stock."
+ : "No inventory record was found for that SKU.";
+
+ return Content($"
Pass route data with individual attributes or one Razor dictionary
+
Pass route values
+
Add route values individually or from a dictionary
hx-route-* and hx-all-route-data
-
Both forms produce a typed Razor Pages URL without composing a query string manually.
+
Both buttons generate a handler URL without assembling a query string.
- Greet via route attribute
+ hx-page-handler="Order"
+ hx-route-id="1042"
+ hx-target="#order-result">
+ Load order #1042
- Greet via route data
+ Load order #1088
-
- Choose a route-data strategy.
+
+ Choose an order.
diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/RouteData.cshtml.cs b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/RouteData.cshtml.cs
index 725af70..d3bb3ff 100644
--- a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/RouteData.cshtml.cs
+++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/RouteData.cshtml.cs
@@ -5,6 +5,6 @@ namespace Ramstack.HtmxToolkit.Demo.Pages.Examples;
public class RouteDataModel : PageModel
{
- public IActionResult OnGetGreet(string name) =>
- Content($"Hello, {name}!");
+ public IActionResult OnGetOrder(int id) =>
+ Content($"Order #{id} is packed and ready for pickup.");
}
diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/ServerEvents.cshtml b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/ServerEvents.cshtml
new file mode 100644
index 0000000..b36417e
--- /dev/null
+++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/ServerEvents.cshtml
@@ -0,0 +1,47 @@
+@page
+@model ServerEventsModel
+@{
+ ViewData["Title"] = "Server events";
+}
+
+
+
+
Example 10
+
Trigger UI updates from the server
+
Notify another element after a successful request without custom JavaScript
+
+
+
+
+
Response.Htmx(h => h.TriggerEvent(...))
+
Saving the form triggers profileSaved; the profile summary listens for that event and reloads itself.
+
+
+
+
+
+ No changes have been saved.
+
+
+
+ The profile summary has not been refreshed.
+
+
+
diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/ServerEvents.cshtml.cs b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/ServerEvents.cshtml.cs
new file mode 100644
index 0000000..fee8c33
--- /dev/null
+++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/ServerEvents.cshtml.cs
@@ -0,0 +1,26 @@
+using System.Text.Encodings.Web;
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace Ramstack.HtmxToolkit.Demo.Pages.Examples;
+
+public class ServerEventsModel : PageModel
+{
+ public IActionResult OnPostSave(string? displayName)
+ {
+ if (string.IsNullOrWhiteSpace(displayName))
+ return BadRequest("Display name is required.");
+
+ Response.Htmx(htmx => htmx.TriggerEvent("profileSaved"));
+ return Content("Profile saved.");
+ }
+
+ public IActionResult OnGetSummary(string? displayName)
+ {
+ if (string.IsNullOrWhiteSpace(displayName))
+ return BadRequest("Display name is required.");
+
+ return Content($"Current profile: {HtmlEncoder.Default.Encode(displayName)}.");
+ }
+}
diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/UrlTagHelper.cshtml b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/UrlTagHelper.cshtml
index 1d2f1e9..f30f205 100644
--- a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/UrlTagHelper.cshtml
+++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/UrlTagHelper.cshtml
@@ -1,40 +1,33 @@
@page
@model UrlTagHelperModel
@{
- ViewData["Title"] = "URL Tag Helper";
+ ViewData["Title"] = "Razor Pages URLs";
}
Example 01
-
URL Tag Helper
-
Generate HTMX URLs with familiar Razor Pages routing attributes
+
Generate Razor Pages URLs
+
Build an HTMX request URL from a Razor Page and handler name
hx-page + hx-page-handler
-
The toolkit generates the request URL and uses hx-get by default.
+
The toolkit generates hx-get, so the URL stays in sync with ASP.NET Core routing.
- Get server time
-
-
- Say hello
+ Check service status
- Run an action to see the response.
+ The service has not been checked yet.
diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/UrlTagHelper.cshtml.cs b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/UrlTagHelper.cshtml.cs
index 5b9599a..2dca39b 100644
--- a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/UrlTagHelper.cshtml.cs
+++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/UrlTagHelper.cshtml.cs
@@ -5,9 +5,6 @@ namespace Ramstack.HtmxToolkit.Demo.Pages.Examples;
public class UrlTagHelperModel : PageModel
{
- public IActionResult OnGetServerTime() =>
- Content($"Server time: {DateTime.Now:HH:mm:ss}");
-
- public IActionResult OnGetHello() =>
- Content("Hello from the server!");
+ public IActionResult OnGetStatus() =>
+ Content($"Service is healthy. Checked at {DateTime.UtcNow:HH:mm:ss} UTC.");
}
diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/_PollingStatus.cshtml b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/_PollingStatus.cshtml
index bed0f74..6c4f832 100644
--- a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/_PollingStatus.cshtml
+++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/_PollingStatus.cshtml
@@ -1,18 +1,53 @@
-@model PollingModel.PollingState
+@model PollingModel.ProgressState
-@if (Model.Stopped)
+@if (Model.Completed)
{
-