Layout areas are named rendering slots registered on a message hub. Each area pairs a name with a view function that produces UI controls. When a client requests an area, the hub runs the matching view function and streams the result — live updates included when the view returns an observable. NODE HUB A Overview WithView("Overview", …) Content WithView("Content", …) Edit WithView("Edit", …) default → "Content" NODE HUB B Overview Splitter view Thumbnail WithView("Thumbnail", …) LayoutAreaControl embeds Hub A area CLIENT requests area ← streams UI controls live-updating LayoutAreaControl Layout areas are named slots on each hub; LayoutAreaControl composes them across node boundaries by embedding one hub's area inside another.

Registering Layout Areas

Add layout areas through the AddLayout() pipeline on MessageHubConfiguration:

public static MessageHubConfiguration AddCodeViews(this MessageHubConfiguration configuration)
    => configuration.AddLayout(layout => layout
        .WithDefaultArea("Content")       // Rendered when no area name is specified
        .WithView("Content", Content)     // Simple read-only view
        .WithView("Overview", Overview)   // Splitter with navigation
        .WithView("Edit", Edit));         // Editor view

Core Registration Methods

Method Purpose
WithDefaultArea(name) Sets which area renders when the caller specifies an empty area name
WithView(name, function) Registers a view function for the named area

View Function Signatures

A view function receives a LayoutAreaHost and RenderingContext. Return a static control for a one-time render, or an IObservable<UiControl?> for a live-updating view:

// Static view — renders once
public static UiControl Edit(LayoutAreaHost host, RenderingContext ctx)
{
    return Controls.Stack.WithView(Controls.H1("Editor"));
}

// Reactive view — re-renders whenever data changes
public static IObservable<UiControl?> Content(LayoutAreaHost host, RenderingContext ctx)
{
    return host.Workspace.GetStream<MeshNode>()
        .Select(nodes => (UiControl?)BuildContent(nodes));
}

Reading the Hosting Node's Own Content

This is the single most-reached-for thing in a NodeType's layout area — "give me my node's content, typed" — and the name authors reach for does not exist.

🚨 LayoutAreaHost has no GetData<T>(). Its whole data surface is GetDataStream<T>(string id) / UpdateData(string id, …), and those address the layout area's own /data/{id} scratch space — form values, a filter, a selection — not the mesh node the hub is hosting. The two are unrelated stores that happen to have similar-looking names.

What makes it cost an afternoon is the diagnostic. host.GetData<MyContent>() still binds, to the unrelated extension WorkspaceOperations.GetData<T>(this EntityStore store) (src/MeshWeaver.Data/WorkspaceOperations.cs), so the compiler does not say "no such method" — it says:

CS1929: 'LayoutAreaHost' does not contain a definition for 'GetData' and the best extension method
        overload 'WorkspaceOperations.GetData<MyContent>(EntityStore)' requires a receiver of type
        'MeshWeaver.Data.EntityStore'

which reads as "you passed the wrong receiver" and sends the reader hunting for an EntityStore to pass. There isn't one to find; the method was never the right one.

The read is the node stream, and the no-argument overload means "this hub's own node":

public static IObservable<UiControl?> Detail(LayoutAreaHost host, RenderingContext _)
    => host.Workspace.GetMeshNodeStream()                       // no argument = the hosting node
        .Select(node => node.ContentAs<MyContent>(host.Hub.JsonSerializerOptions))
        .Select(content => (UiControl?)Controls.Stack
            .WithView(Controls.H2(content?.Name ?? ""))
            .WithView(Controls.Markdown(content?.Description ?? "")));

host.Workspace.GetMeshNodeStream() is the shipped idiom — ExportLayoutArea, MarkdownOverviewLayoutArea, MeshNodeLayoutAreas and UserActivityLayoutAreas all open with it — and GetMeshNodeStream(path) reads any other node through the same process-wide IMeshNodeStreamCache.

Three rules travel with it:

Any literal a viewer reads — the empty-state text the ?? fallbacks elide above — goes through host.Localize("key") with the key in both strings.en.json and strings.de.json; see Cross-Renderer Authoring.

See Data Binding, CQRS and Content Access and Requesting Work via stream.Update().

Use LayoutAreaReference and ToHref() to build navigation links between areas on the same hub or across node boundaries:

// Navigate to the Edit area of the current hub
var editHref = new LayoutAreaReference("Edit").ToHref(hubAddress);
Controls.Button("Edit").WithNavigateToHref(editHref);

// Navigate to a specific area on a different node
var overviewHref = new LayoutAreaReference("Overview").ToHref(otherNodePath);
new NavLinkControl("View Code", FluentIcons.Code(), overviewHref);

Composing Views Across Hubs with LayoutAreaControl

LayoutAreaControl embeds a layout area from one hub inside another hub's view. This is the primary mechanism for composing UI across node boundaries:

// Embed the default area of a target hub
new LayoutAreaControl(targetAddress, new LayoutAreaReference(""))

// Embed a specific named area
new LayoutAreaControl(targetAddress, new LayoutAreaReference("Thumbnail"))

How the Default Area Resolves (and Avoids Infinite Recursion)

When the LayoutAreaReference has an empty area name, the target hub resolves its default area — the one registered via WithDefaultArea. This behaviour is the key to safe composition: an Overview area can embed a Content pane without looping back to itself.

Example — Code node Overview Splitter

The Code node sets Content as its default area (a simple markdown code block) and Overview as a Splitter. The Splitter's right pane uses LayoutAreaControl(address, ""), which resolves to Content — not back to Overview — so there is no infinite recursion:

// In AddCodeViews:
.WithDefaultArea("Content")     // Default = simple code block
.WithView("Content", Content)   // Simple markdown view
.WithView("Overview", Overview) // Splitter with code list + embedded Content

// In the Overview Splitter's right pane:
new LayoutAreaControl(hubAddress, new LayoutAreaReference(""))
// ^ Resolves to "Content" (the default), NOT "Overview"

Embedding an area inside a container control

The two mechanisms in Combining Layout Areas — the @@ live embed and the --render cell — put an area into markdown flow. A layout can also emit a LayoutAreaControl directly as a child of a container control, which is what a page composed of blocks does:

Controls.Stack
    .WithView(Controls.Markdown(intro), "Intro")
    .WithView(new LayoutAreaControl(new Address(childPath), new LayoutAreaReference("")), "Cell")
    .WithView(Controls.Markdown(epilogue), "Epilogue");

Controls.Stack renders a flex box, so the embedded area is a flex item. Two sizing contracts follow, and the framework — not the author — selects between them:

Context How it is sized
Top-level area (a routed page, or the side panel) Fills the available height. Marked with the fill-area class.
Embedded area (anything else) Sized by its own content, so the blocks around it flow normally.

An embedded area must never be given a zero flex basis or lose its min-content floor: its box would collapse while its content renders at full height, and the content would paint over the block below it. LayoutAreaView.razor.css keeps the fill sizing behind .fill-area for exactly this reason.

The example below is that composition, rendered live — the embed takes its natural height and the text beneath it stays clear of it:

using MeshWeaver.Layout;
using MeshWeaver.Data;        // LayoutAreaReference
using MeshWeaver.Messaging;   // Address — the kernel pre-imports neither

// Two things this example pins down, both easy to get wrong:
//  • the address must be an Address, not a bare string — the parameter is typed `object`, so a
//    string compiles and then silently renders an empty area;
//  • prefer naming the area. An empty reference resolves to the target's DEFAULT area, as
//    described above — which is whatever that node registered, not necessarily the one you
//    want. Embedding this page's default rendered nothing; "Overview" renders its content.
Controls.Stack
    .WithView(Controls.Markdown("**Above the embed.**"))
    .WithView(new LayoutAreaControl(
        new Address("Doc/GUI/DataGrid"), new LayoutAreaReference("Overview")))
    .WithView(Controls.Markdown("**Below the embed** — this text must not be painted over."))

Principle: Define Layout Areas Close to Their Object

Layout areas belong in the same module as the object they represent. This keeps each node type self-contained and composable — a parent never needs to know how a child node renders itself, only which area to link to.

// In NodeTypeLayoutAreas — the parent just links to the Code node's own area:
var codeHref = new LayoutAreaReference(CodeLayoutAreas.OverviewArea)
    .ToHref(codeNode.Path);
new NavLinkControl(codeNode.Name, icon, codeHref);

The Code node handles all of its own display logic. The parent simply points at it.

Common Patterns

Splitter with Nav Menu and Content Pane

A horizontal splitter with a collapsible left navigation and a fluid content area — used by NodeType and Code nodes:

Controls.Splitter
    .WithSkin(s => s.WithOrientation(Orientation.Horizontal)
        .WithWidth("100%").WithHeight("calc(100vh - 100px)"))
    .WithView(
        BuildNavMenu(),   // Left: navigation menu
        skin => skin.WithSize("280px").WithCollapsible(true))
    .WithView(
        BuildContent(),   // Right: main content
        skin => skin.WithSize("*"));

Read-Only View with Edit Button

Display content alongside a button that navigates to the Edit area:

var editHref = new LayoutAreaReference("Edit").ToHref(hubAddress);

Controls.Stack
    .WithView(Controls.H1(title))
    .WithView(Controls.Button("")
        .WithIconStart(FluentIcons.Edit())
        .WithNavigateToHref(editHref));

Edit View with Save and Cancel

An editor that commits changes and redirects back to the Overview:

Controls.Button("Save").WithClickAction(ctx =>
{
    // ... save logic — compose it as an observable and Subscribe; never await here ...
    var viewHref = new LayoutAreaReference("Overview").ToHref(hubAddress);
    ctx.Host.UpdateArea(ctx.Area, new RedirectControl(viewHref));
    return Task.CompletedTask;
});

var cancelHref = new LayoutAreaReference("Overview").ToHref(hubAddress);
Controls.Button("Cancel").WithNavigateToHref(cancelHref);

🚨 The handler is synchronousctx => { …; return Task.CompletedTask; }, never async ctx =>. An async click handler runs its continuation on the wrong scheduler and deadlocks the layout pump under load. Work that needs I/O is composed as an IObservable<T> and .Subscribe(...)d from inside the handler; the handler itself returns immediately. See Observables and Asynchronous Calls.

Live Demo

The cell below shows a self-contained layout area composed from a stack of controls — the same building blocks used for every area above:

MeshWeaver.Layout.Controls.Stack
    .WithView(MeshWeaver.Layout.Controls.Html("<strong>Layout Area — live render</strong>"))
    .WithView(MeshWeaver.Layout.Controls.Markdown(
        $"This area was rendered at **{DateTime.Now:HH:mm:ss}**. " +
        "In a real hub it would update reactively whenever its data changes."))
    .WithView(MeshWeaver.Layout.Controls.Button("Navigate to Edit area"))

See Also

Reconnecting…
The server was updated. Reloading the page to pick up the latest version.