Skip to content

DeferredNode

A layout node that delegates to another node without owning or disposing it. Essential for showing/hiding modals and other components that should persist across layout changes.

The Problem

When using reactive layouts with AsLayout(), the previous layout node is disposed whenever a new value is emitted:

csharp
// Problem: Modal gets disposed when hidden!
ViewModel.ShowModalChanged
    .Select(show => show ? ViewModel.Modal : Layouts.Empty())
    .AsLayout()  // When show becomes false, Modal is disposed!

This causes ObjectDisposedException when you try to show the modal again.

The Solution

DeferredNode wraps access to a node without taking ownership:

csharp
// Solution: Use Layouts.Deferred()
ViewModel.ShowModalChanged
    .Select(show => show
        ? Layouts.Deferred(() => ViewModel.Modal)  // Doesn't dispose the modal
        : (ILayoutNode)Layouts.Empty())
    .AsLayout()

Basic Usage

csharp
// Create a node you want to reuse
var modal = Layouts.Modal()
    .WithTitle("My Modal")
    .WithContent(content);

// In your layout, wrap it with Deferred
var layout = Layouts.Stack()
    .WithChild(mainContent)
    .WithChild(
        showModalObservable
            .Select(show => show
                ? Layouts.Deferred(() => modal)
                : (ILayoutNode)Layouts.Empty())
            .AsLayout());

How It Works

  • DeferredNode calls the provided factory function on each render
  • When DeferredNode is disposed (layout changes), it does NOT dispose the underlying node
  • The underlying node's lifecycle is managed by its owner (typically a ViewModel)

Complete Example

csharp
public partial class MyViewModel : ReactiveViewModel
{
    [Reactive] private bool _showConfirmation;

    private ModalNode? _confirmModal;

    public ModalNode? ConfirmModal => _confirmModal;

    public override void OnActivated()
    {
        _confirmModal = Layouts.Modal()
            .WithTitle("Confirm")
            .WithContent(new TextNode("Are you sure?"));

        _confirmModal.Dismissed
            .Subscribe(_ => ShowConfirmation = false)
            .DisposeWith(Subscriptions);
    }

    protected override void Dispose(bool disposing)
    {
        // ViewModel owns the modal and disposes it here
        _confirmModal?.Dispose();
        base.Dispose(disposing);
    }
}

public class MyPage : ReactivePage<MyViewModel>
{
    public override ILayoutNode BuildLayout()
    {
        return Layouts.Stack()
            .WithChild(BuildMainContent())
            .WithChild(
                // Deferred prevents disposal when modal is hidden
                ViewModel.ShowConfirmationChanged
                    .Select(show => show
                        ? Layouts.Deferred(() => ViewModel.ConfirmModal)
                        : (ILayoutNode)Layouts.Empty())
                    .AsLayout());
    }
}

When to Use DeferredNode

Use Layouts.Deferred() when:

  1. Modals - Components that show/hide but should retain state
  2. Tabs or panels - Content that switches but shouldn't reset
  3. Cached views - Pre-rendered content you want to reuse
  4. Any node owned by ViewModel - Where lifecycle is managed externally

When NOT to Use DeferredNode

Don't use Layouts.Deferred() when:

  1. Creating new nodes each time - Just return the node directly
  2. Nodes should be recreated - Fresh state on each show
  3. Simple conditional content - Use When.True() or ConditionalNode

API Reference

Factory Method

csharp
Layouts.Deferred(Func<ILayoutNode?> getNode)

Behavior

MethodDeferredNode Behavior
Measure()Delegates to underlying node
Render()Delegates to underlying node
Dispose()Does nothing - node is NOT disposed

Properties

PropertyBehavior
WidthConstraintReturns underlying node's constraint (or AutoSize if null)
HeightConstraintReturns underlying node's constraint (or AutoSize if null)

Source Code

View DeferredNode implementation
csharp
// Copyright (c) Petabridge, LLC. All rights reserved.
// Licensed under the Apache 2.0 license. See LICENSE file in the project root for full license information.

using Termina.Rendering;

namespace Termina.Layout;

/// <summary>
/// A layout node that delegates to a lazily-obtained node without owning or disposing it.
/// </summary>
/// <remarks>
/// <para>
/// Use this wrapper when you need to render a node that is owned elsewhere (like a ViewModel)
/// and should not be disposed when the layout changes. This is particularly useful for modals
/// and other overlay components that need to be shown/hidden without being recreated.
/// </para>
/// <para>
/// Example usage with reactive layouts:
/// <code>
/// ViewModel.ShowModalChanged
///     .Select(show => show
///         ? new DeferredNode(() => ViewModel.MyModal)
///         : Layouts.Empty())
///     .AsLayout()
/// </code>
/// </para>
/// </remarks>
public sealed class DeferredNode : ILayoutNode
{
    private readonly Func<ILayoutNode?> _getNode;

    /// <summary>
    /// Creates a new DeferredNode that delegates to the node returned by the factory.
    /// </summary>
    /// <param name="getNode">A function that returns the node to render, or null for no content.</param>
    public DeferredNode(Func<ILayoutNode?> getNode)
    {
        _getNode = getNode ?? throw new ArgumentNullException(nameof(getNode));
    }

    /// <inheritdoc />
    public SizeConstraint WidthConstraint => _getNode()?.WidthConstraint ?? SizeConstraint.AutoSize();

    /// <inheritdoc />
    public SizeConstraint HeightConstraint => _getNode()?.HeightConstraint ?? SizeConstraint.AutoSize();

    /// <inheritdoc />
    public Size Measure(Size available)
    {
        return _getNode()?.Measure(available) ?? Size.Zero;
    }

    /// <inheritdoc />
    public void Render(IRenderContext context, Rect bounds)
    {
        _getNode()?.Render(context, bounds);
    }

    /// <summary>
    /// Does not dispose the underlying node - it is owned by the caller.
    /// </summary>
    public void Dispose()
    {
        // Intentionally empty - we don't own the node
    }
}

Released under the Apache 2.0 License.