Skip to documentation
Heimdall Docs
Heimdall Docs

Razor, Pages & Components

Heimdall works with the Razor surfaces you already use: MVC views, Razor Pages, partials, View Components, Tag Helpers, and static Razor components.

The important boundary stays the same: the server renders HTML, Heimdall sends the next HTML fragment, and the browser updates a declared target.

1. Razor Is A Rendering Surface

Heimdall does not require a particular server-side templating style. It only needs the final HTML to contain the Heimdall attributes that describe the interaction. Razor can therefore remain responsible for composition while Heimdall handles the request and swap lifecycle.

Text
Razor view or partial
    -> rendered HTML with Heimdall attributes
    -> browser runtime sends a content request
    -> action returns HTML
    -> declared target is updated

2. Register MVC Or Razor Pages

Use the normal ASP.NET Core registration for the Razor host you have. AddHeimdallMvc adds the MVC view services used when a content action renders a Razor partial; it is useful for MVC apps and Razor Pages apps that want the same partial renderer.

C#
builder.Services.AddControllersWithViews();
// Or, for Razor Pages:
builder.Services.AddRazorPages();

builder.Services.AddHeimdall();
builder.Services.AddHeimdallMvc();

var app = builder.Build();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();

app.UseHeimdall();

3. Use Heimdall Attributes In A Razor View

Native attributes are often the clearest choice in a .cshtml file. They can sit beside normal Razor expressions, tag helpers, forms, partials, and layout markup.

Text
@model OrderFilter

<form
  id="order-filter"
  heimdall-content-submit="orders.filter"
  heimdall-payload-from="closest-form"
  heimdall-content-target="#orders-table"
  heimdall-content-swap="inner">
  <input name="Search" value="@Model.Search" />
  <button type="submit">Apply</button>
</form>

<div id="orders-table">
  @await Html.PartialAsync("_OrderList", Model)
</div>

4. Razor Pages Work The Same Way

Razor Pages can use Heimdall attributes in the page markup and can keep ordinary OnGet or OnPost handlers for normal navigation and form posts. Heimdall content actions remain named HTML-returning interactions, so they do not need to be forced into a PageModel handler shape.

Text
@page
@model OrdersModel

<section id="orders-panel">
  <button
    heimdall-content-click="orders.refresh"
    heimdall-content-target="#orders-panel"
    heimdall-content-swap="outer">
    Refresh
  </button>

  @await Html.PartialAsync("_OrderList", Model.Orders)
</section>

5. Return Partials From Content Actions

Partials are the simplest reusable fragment boundary. Render the same partial during the initial page request and from the content action so both paths use the same markup and Heimdall attributes.

Code
[ContentInvocationPrefix("orders")]
public sealed class OrderActions(
    IOrderRepository orders,
    IHeimdallMvcRenderer views)
{
    [ContentInvocation("filter")]
    public async Task<IHtmlContent> Filter(
        OrderFilter filter,
        CancellationToken ct)
    {
        var results = await orders.SearchAsync(filter, ct);
        return await views.PartialAsync(
            "_OrderList",
            results,
            ct);
    }
}

6. Partials Can Compose Partials

The MVC renderer uses the real view engine. A returned partial can use nested partials, layouts-owned helpers, Razor dependency injection, tag helpers, ViewData, and TempData according to the normal MVC rendering rules.

Text
// Views/Shared/_OrderList.cshtml
@model IReadOnlyList<OrderRow>

<div class="order-list">
  @foreach (var order in Model)
  {
    @await Html.PartialAsync("_OrderRow", order)
  }
</div>

7. View Components Are Still Available

Use an MVC View Component when a reusable server-rendered region owns its own query and composition logic. It can be invoked from a Razor view or partial like any other View Component. Heimdall can trigger an action that returns a partial containing that invocation.

C#
// In a Razor view or partial.
<aside id="cart-summary">
  @await Component.InvokeAsync(
      "CartSummary",
      new { CartId = Model.CartId })
</aside>

// A content action can return a partial such as
// _CartSummaryHost.cshtml, which invokes the same
// View Component.
return await views.PartialAsync(
    "_CartSummaryHost",
    cart,
    ct);

8. Tag Helpers And Dependency Injection

Tag Helpers run as part of Razor rendering. They can generate the markup that Heimdall consumes, and they can coexist with explicit Heimdall attributes. Razor views and partials can also continue to inject application services in the normal way.

Text
@inject ICurrencyFormatter Currency

<form
  method="post"
  asp-page-handler="Save"
  heimdall-content-submit="profile.save"
  heimdall-payload-from="closest-form"
  heimdall-content-target="#profile-form"
  heimdall-content-swap="outer">
  <input asp-for="Model.DisplayName" />
  <span>@Currency.Format(Model.Balance)</span>
  <button type="submit">Save</button>
</form>

9. Controller-Local Actions

In an MVC-heavy application, a content action can live beside the controller and view it serves. Mark it NonAction so MVC conventional routing does not expose it as a normal controller action.

JSON
[ContentInvocationPrefix("orders")]
public sealed class OrdersController(
    IOrderRepository orders,
    IHeimdallMvcRenderer views) : Controller
{
    public IActionResult Index() => View();

    [NonAction]
    [ContentInvocation("filter")]
    public async Task<IHtmlContent> FilterRows(
        OrderFilter filter,
        CancellationToken ct)
    {
        var results = await orders.SearchAsync(filter, ct);
        return await views.PartialAsync(
            "_OrderList",
            results,
            ct);
    }
}

10. Static Razor Components

Modern Razor components (.razor files, often called Blazor components) can be rendered as static HTML from a Razor view or partial. This is a useful bridge when a component already owns a complex markup tree but does not need a live client-side circuit.

C#
// _OrderSummary.cshtml
@model Order

<component
    type="typeof(OrderSummary)"
    render-mode="Static"
    param-Order="Model" />

// An action can return the partial just like any
// other Heimdall HTML fragment.
return await views.PartialAsync(
    "_OrderSummary",
    order,
    ct);

11. Parent And Child Components

A statically rendered parent component renders its complete descendant tree. Child components, nested render fragments, parameters, and their server-side rendering logic are included in the returned HTML; they do not need individual Heimdall actions.

C#
// OrderSummary.razor
<OrderHeader Order="Order" />
<OrderTotals Order="Order" />
<OrderActions OrderId="Order.Id" />

@code {
    [Parameter] public Order Order { get; set; } = default!;
}

// One content action can return the parent host.
// The complete static component tree is rendered.

12. Interactive Blazor Is An Island

Interactive Blazor components can coexist with Heimdall, but the ownership boundary should be explicit. Let Blazor own the subtree containing its component state and let Heimdall own neighboring server-rendered regions. Do not routinely swap Heimdall HTML into a live Blazor subtree or let Blazor rewrite a Heimdall-owned target.

Text
Page shell
|- Heimdall-owned #activity-feed
|- Blazor-owned <LiveEditor @rendermode="InteractiveServer" />
`- Heimdall-owned #notifications

Heimdall updates its own targets.
Blazor updates its own component subtree.

13. Choose The Smallest Useful Boundary

Use the rendering primitive that matches the ownership of the region. The goal is not to convert every view into one component system; it is to make each server-rendered boundary easy to return and update.

Text
Partial:
- small reusable HTML fragment
- ideal for Heimdall swaps

View Component:
- reusable server-side query and composition
- useful when the region needs DI-backed logic

Static Razor component:
- existing .razor component rendered as HTML
- parent and children render together

Interactive Blazor component:
- live component state and event handling
- keep it inside an explicit Blazor-owned island

14. Keep The Action And Render Boundary Together

When a partial, View Component host, or static Razor component owns a target, keep its action ID, payload model, target ID, and returned markup close together. This makes the next HTML state obvious and avoids actions that quietly update unrelated regions.

Text
Component or partial boundary:
- stable host id
- Heimdall action id
- payload/state model
- returned partial or component host

event -> action -> owned HTML -> owned target

Next Steps

Start with the MVC integration page for application wiring, then use the actions, forms, and patterns pages to shape the interactions around each Razor boundary.