The Event System
Decouple your UI components using Server-Side Events and Distributed Handlers.
The Monolith Problem
In a typical MVC app, controllers often become "God Objects" that know too much about the UI.
// ❌ Introduction to tight coupling
public IActionResult CompleteTask(int id)
{
// Business Logic
_service.Complete(id);
// UX Logic - explicitly knowing every part of the page that needs an update
var builder = SwapResponse();
builder.AlsoUpdate("task-list", "_List", _service.GetTasks()); // Update List
builder.AlsoUpdate("sidebar-stats", "_Stats", _service.GetStats()); // Update Sidebar
builder.AlsoUpdate("header-alert", "_Alert", "Done!"); // Show Alert
return builder.Build(); // The controller is now coupled to 3 separate UI widgets.
}
The Solution: Event-Driven Responses
Swap.Htmx allows you to invert this dependency. The Controller triggers an Event, and independent Handlers decide how to update the UI.
Pattern: Distributed Handlers
1. Define the Event
Simple C# classes representing what happened.
public record TaskCompletedEvent(int TaskId);
2. Updates in Isolation
Create small, focused handlers for each UI component.
// Handler 1: The List Component removes the item
[SwapHandler]
public class TaskListHandler : ISwapEventHandler<TaskCompletedEvent>
{
public Task HandleAsync(TaskCompletedEvent evt, SwapResponseBuilder builder, CancellationToken ct)
{
// Tell HTMX to remove the element with this ID
builder.AlsoUpdate(SwapElements.TaskRow(evt.TaskId), "", null, SwapMode.Delete);
return Task.CompletedTask;
}
}
// Handler 2: The Sidebar Component updates its counter
[SwapHandler]
public class SidebarHandler : ISwapEventHandler<TaskCompletedEvent>
{
public Task HandleAsync(TaskCompletedEvent evt, SwapResponseBuilder builder, CancellationToken ct)
{
var stats = _repo.GetStats();
builder.AlsoUpdate(SwapElements.SidebarStats, SwapViews.Sidebar._Stats, stats);
return Task.CompletedTask;
}
}
3. The Clean Controller
The controller simply announces what happened.
[HttpPost]
public IActionResult Complete(int id)
{
_service.Complete(id);
return SwapEvent(new TaskCompletedEvent(id)).Build();
}
Benefits
- Single Responsibility: Controllers focus on user intent, Handlers focus on UI updates.
- Scalability: Add a new UI widget? Just add a new Handler. No need to touch the Controller.
- Testing: Unit test your handlers in isolation to verify they update the correct target IDs.
Source Generators
Use our Source Generators to avoid magic strings for Event Names and Element IDs.
// Auto-generated constants
builder.AlsoUpdate(SwapElements.SidebarStats, ...);