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.
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:
- 🚨 Never cast the content.
node.Content is MyContentis a trap-door that yields a silent null for the three cases that actually occur in a live mesh — untyped JSON from an unresolvable$type, the as-writtenJsonObjectbefore materialization, and a same-named type from another collectible assembly (every NodeType recompile mints one). The view renders empty with no exception and nothing to grep. UseContentAs<T>(hub.JsonSerializerOptions). The shippedSocialMediaProfileLayoutAreassample predates this and hand-digs aJsonElementthrough aGetProphelper — that helper is the symptom, not a pattern to copy. - 🚨 Never
.Take(1)on the stream feeding the returned view — it freezes the binding at the first emission and the area stops tracking the node. - Writing back is
GetMeshNodeStream(path).Update(current => current with { … }), the only mutation API — and it returns a cold observable, so the write does not happen until you.Subscribe(_ => { }, ex => logger.LogWarning(ex, …)). Do it from a synchronous click handler (see the 🚨 note under Edit View with Save and Cancel below); neverasync ctx =>.
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().
Navigating Between Areas
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
Contentas its default area (a simple markdown code block) andOverviewas a Splitter. The Splitter's right pane usesLayoutAreaControl(address, ""), which resolves toContent— not back toOverview— 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.
- Code views are defined in
CodeLayoutAreasand registered viaAddCodeViews()on Code node hubs - Markdown views are defined in
MarkdownLayoutAreasand registered viaAddMarkdownViews() - NodeType views are defined in
NodeTypeLayoutAreasand registered viaAddNodeTypeView()
// 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 synchronous —
ctx => { …; return Task.CompletedTask; }, neverasync 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 anIObservable<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
- Adding Controls to a UI — Container types and
WithView - Data Binding — How data flows between server and UI
- Adding Editable Forms — Auto-generated forms from records
- Observables — Reactive views that update when data changes
- Slides & Decks — the Slide and Deck node types, with slide order declared externally in the deck