Skip to content

Hierarchical Family

When To Use It

Use HierarchicalVM<TModel, TVM> when the domain is intrinsically recursive: folders, notebooks, comment threads, taxonomies, or any tree whose nodes need parent, depth, and path semantics.

This family exists so you do not have to manually recurse a CompositeVM and rebuild tree bookkeeping in every app.

Hierarchical Family Map

HTML · SVG · PNG

Shape And Ownership

A hierarchical node owns:

  • its Model
  • its recursive Children
  • structural metadata such as Parent, Depth, Path, IsRoot, and IsLeaf

Children are lazy by default and can be materialized eagerly where the host needs the whole tree available during construct.

Lifecycle And Messaging

The important differences from manual recursive composites are:

  • eager trees construct depth-first across the full subtree
  • lazy children do not participate until materialized
  • structural mutations publish TreeStructureChangedMessage
  • cache invalidation publishes property-changed for the children property

The family integrates directly with walk and walk_expanded.

Cross-Language Surface

Concept C# Python TypeScript Swift
Core type HierarchicalVM<TModel, TVM> HierarchicalVM[TModel, TVM] HierarchicalVM<TModel, TVM> HierarchicalVM<TModel, TVM>
Builder HierarchicalVMBuilder<TModel, TVM> HierarchicalVMBuilder[TModel, TVM] HierarchicalVMBuilder<TModel, TVM> HierarchicalVM<TModel, TVM>.builder()
Eager flag EagerChildren(true) eager_children(True) eagerChildren(true) eagerChildren(true)
Invalidate subtree InvalidateSubtree() invalidate_subtree() invalidateSubtree() invalidateSubtree()

Example

The current Notes Workspace examples do not subclass HierarchicalVM in any flavor. Each NotebooksRootVM owns a flat notebook collection plus roots / childrenOf / walk projections and republishes TreeStructureChangedMessage, which gives the host tree-shaped state without making the VM itself a recursive node tree.

Use HierarchicalVM when the VM graph should actually be recursive:

These concise examples show the canonical builder path per flavor. C#, Swift, and TypeScript supply a TestNode factory for their concrete recursive node; Python can build plain HierarchicalVM directly and uses vm_factory(...) only when a subclass is required.

var root = HierarchicalVMBuilder<string, TestNode>.Empty
    .Model("root")
    .ChildrenFactory(_ => Array.Empty<TestNode>())
    .Services(hub, dispatcher)
    .VmFactory(ctx => new TestNode(
        ctx.Model, ctx.ChildrenFactory, ctx.Hub, ctx.Dispatcher,
        ctx.Name, ctx.Hint, ctx.EagerChildren))
    .Build();
root = (
    HierarchicalVMBuilder()
    .model("root")
    .children_factory(lambda _parent: [])
    .services(hub, dispatcher)
    .build()
)
const root = new HierarchicalVMBuilder<string, TestNode>()
  .model("root")
  .childrenFactory((_parent) => [])
  .services(hub, dispatcher)
  .vmFactory((ctx) => new TestNode(ctx))
  .build();
let root = try HierarchicalVM<String, TestNode>.builder()
    .model("root")
    .childrenFactory { _ in [] }
    .services(hub: hub, dispatcher: dispatcher)
    .vmFactory { model, childrenFactory, hub, dispatcher, name, hint, eager in
        TestNode(
            model: model,
            childrenFactory: childrenFactory,
            hub: hub,
            dispatcher: dispatcher,
            name: name,
            hint: hint,
            eagerChildren: eager
        )
    }
    .build()

That split is the practical rule: use the tree primitive when the tree is real, not simply because the UI displays indentation.

Common Pitfalls

  • Recursing CompositeVM instead of using the tree primitive and then having to rebuild Parent, Depth, and Path yourself.
  • Expecting lazy children to exist during construct without materializing them.
  • Treating cache invalidation as structural mutation. It is a refresh contract, not an add/remove event.
  • Reparenting a node under itself or its descendants. The contract rejects these cycles.