Skip to content

Navigation

Termina provides navigation actions within ViewModels to move between pages.

ViewModels have access to protected navigation methods wired up by the framework:

Navigate directly by path:

csharp
Navigate("/");           // Go to home
Navigate("/settings");   // Go to settings
Navigate("/items/42");   // Go to item 42

Navigate using a template with parameters:

csharp
NavigateWithParams("/items/{id}", new { id = 42 });
NavigateWithParams("/users/{name}", new { name = "alice" });

Shutdown

Request graceful application shutdown:

csharp
Shutdown();  // Exits the application
csharp
public partial class MenuViewModel : ReactiveViewModel
{
    [Reactive] private int _selectedIndex;

    public override void OnActivated()
    {
        Input.OfType<KeyPressed>()
            .Subscribe(HandleKey)
            .DisposeWith(Subscriptions);
    }

    private void HandleKey(KeyPressed key)
    {
        switch (key.KeyInfo.Key)
        {
            case ConsoleKey.Enter:
                NavigateToSelected();
                break;
            case ConsoleKey.Escape:
                Shutdown();
                break;
        }
    }

    private void NavigateToSelected()
    {
        var route = SelectedIndex switch
        {
            0 => "/counter",
            1 => "/todo",
            2 => "/settings",
            _ => "/"
        };
        Navigate(route);
    }
}

When registering routes, you can control how navigation behaves:

csharp
termina.RegisterRoute<DetailPage, DetailViewModel>(
    "/items/{id}",
    NavigationBehavior.PreserveState);  // Keep ViewModel state

ResetOnNavigation (Default)

  • Creates new Page and ViewModel on each navigation
  • State is reset each time
  • Use for pages that should start fresh

PreserveState

  • Reuses existing Page and ViewModel instances
  • State persists across navigations
  • Layout tree is preserved and reactivated, not disposed
  • ViewModel lifecycle:
    • OnDeactivating() disposes subscriptions
    • OnActivated() recreates subscriptions
  • Layout lifecycle:
    • OnDeactivate() pauses timers, stops animations, pauses observable subscriptions
    • OnActivate() resumes timers, animations, and subscriptions
  • Use for pages with expensive state, data caching, or complex UI state (e.g., form inputs, scroll positions)

Lifecycle Methods

OnActivated

Called when navigating to the page:

csharp
public override void OnActivated()
{
    // Setup subscriptions
    Input.OfType<KeyPressed>()
        .Subscribe(HandleKey)
        .DisposeWith(Subscriptions);

    // Load data
    LoadItems();
}

OnDeactivating

Called when navigating away:

csharp
public override void OnDeactivating()
{
    // Subscriptions are auto-disposed
    base.OnDeactivating();  // Important: call base

    // Optional: save state, cancel operations
    SaveDraft();
}

RequestRedraw

For asynchronous content updates (like streaming), request a UI refresh:

csharp
private async Task StreamDataAsync()
{
    await foreach (var chunk in dataStream)
    {
        Messages = Messages.Append(chunk).ToList();
        RequestRedraw();  // Trigger UI update
    }
}

ViewModel Source Code

View ReactiveViewModel implementation
csharp
using System.Reactive.Disposables;
using Termina.Input;

namespace Termina.Reactive;

/// <summary>
/// Base class for reactive view models in Termina.
/// ViewModels contain application state and logic, exposing observable properties
/// that pages subscribe to for automatic UI updates.
/// </summary>
/// <remarks>
/// <para>
/// ReactiveViewModel is the "ViewModel" in MVVM pattern. It:
/// </para>
/// <list type="bullet">
///   <item>Owns application state as observable properties (use [Reactive] attribute)</item>
///   <item>Subscribes to input events and backend services</item>
///   <item>Provides navigation and shutdown actions</item>
///   <item>Manages subscription lifecycle via CompositeDisposable</item>
/// </list>
/// <para>
/// Properties marked with [Reactive] get source-generated BehaviorSubject backing.
/// Pages subscribe to the generated *Changed observables to update UI automatically.
/// </para>
/// </remarks>
/// <example>
/// <code>
/// public partial class CounterViewModel : ReactiveViewModel
/// {
///     [Reactive] private int _count;
///
///     public CounterViewModel(IObservable&lt;IInputEvent&gt; input)
///     {
///         input.OfType&lt;KeyPressed&gt;()
///             .Subscribe(HandleKey)
///             .DisposeWith(Subscriptions);
///     }
///
///     private void HandleKey(KeyPressed key)
///     {
///         if (key.KeyInfo.Key == ConsoleKey.UpArrow)
///             Count++;
///     }
/// }
/// </code>
/// </example>
public abstract class ReactiveViewModel : IDisposable
{
    private CompositeDisposable _subscriptions = new();

    /// <summary>
    /// Composite disposable for managing subscriptions.
    /// Use <see cref="RxExtensions.DisposeWith{T}"/> to add subscriptions.
    /// </summary>
    /// <remarks>
    /// <para>
    /// Subscriptions added in <see cref="OnActivated"/> are automatically disposed when
    /// <see cref="OnDeactivating"/> is called. This prevents duplicate subscriptions when using
    /// <see cref="Pages.NavigationBehavior.PreserveState"/>.
    /// </para>
    /// <para>
    /// For subscriptions that should persist across activations (e.g., subscriptions created in
    /// the constructor), store the disposable manually and dispose it in <see cref="Dispose"/>.
    /// </para>
    /// </remarks>
    protected CompositeDisposable Subscriptions => _subscriptions;

    /// <summary>
    /// Navigate to another page by path.
    /// Set by the framework when the ViewModel is bound to a page.
    /// </summary>
    /// <example>
    /// <code>
    /// Navigate("/todos/42");
    /// Navigate("/");
    /// </code>
    /// </example>
    protected Action<string> Navigate { get; private set; } = _ => { };

    /// <summary>
    /// Navigate to another page using a route template and values.
    /// Set by the framework when the ViewModel is bound to a page.
    /// </summary>
    /// <example>
    /// <code>
    /// NavigateWithParams("/todos/{id}", new { id = 42 });
    /// </code>
    /// </example>
    protected Action<string, object?> NavigateWithParams { get; private set; } = (_, _) => { };

    /// <summary>
    /// Request graceful application shutdown.
    /// Set by the framework when the ViewModel is bound to a page.
    /// </summary>
    protected Action Shutdown { get; private set; } = () => { };

    /// <summary>
    /// Request a UI redraw. Use this when content changes asynchronously
    /// (e.g., from streaming data) and the display needs to be refreshed.
    /// Public to allow Pages to trigger redraws on layout invalidation events.
    /// </summary>
    public Action RequestRedraw { get; private set; } = () => { };

    /// <summary>
    /// Observable stream of input events from the application.
    /// Subscribe to this in the ViewModel to handle keyboard input,
    /// or access from the Page to route input to interactive layout nodes.
    /// </summary>
    public IObservable<IInputEvent> Input { get; private set; } = null!;

    /// <summary>
    /// Request graceful application shutdown.
    /// Called by Pages in response to user input (e.g., Ctrl+Q, Escape).
    /// </summary>
    /// <remarks>
    /// Override this method to add custom shutdown behavior such as
    /// confirmation dialogs, saving state, or cleanup operations.
    /// The default implementation calls <see cref="Shutdown"/> directly.
    /// </remarks>
    public virtual void RequestShutdown() => Shutdown();

    /// <summary>
    /// Called when the page becomes active (navigated to).
    /// Override to perform initialization that should happen each time the page is shown.
    /// </summary>
    public virtual void OnActivated()
    {
    }

    /// <summary>
    /// Called when the page is being deactivated (navigating away).
    /// Override to perform cleanup or state saving.
    /// </summary>
    /// <remarks>
    /// The base implementation disposes all <see cref="Subscriptions"/> to prevent
    /// duplicate subscriptions when using <see cref="Pages.NavigationBehavior.PreserveState"/>.
    /// If you override this method, always call the base implementation.
    /// </remarks>
    public virtual void OnDeactivating()
    {
        // Dispose subscriptions and create a new container for next activation
        // This prevents duplicate subscriptions with PreserveState navigation
        _subscriptions.Dispose();
        _subscriptions = new CompositeDisposable();
    }

    /// <summary>
    /// Disposes all subscriptions.
    /// Called by the framework when the ViewModel is no longer needed.
    /// </summary>
    public virtual void Dispose()
    {
        _subscriptions.Dispose();
        GC.SuppressFinalize(this);
    }

    /// <summary>
    /// Wires up the navigation, shutdown actions, and input observable.
    /// Called by the framework when binding to a page.
    /// </summary>
    internal void WireUp(
        Action<string> navigate,
        Action<string, object?> navigateWithParams,
        Action shutdown,
        Action requestRedraw,
        IObservable<IInputEvent> input)
    {
        Navigate = navigate;
        NavigateWithParams = navigateWithParams;
        Shutdown = shutdown;
        RequestRedraw = requestRedraw;
        Input = input;
    }
}

Released under the Apache 2.0 License.