Navigation
Termina provides navigation actions within ViewModels to move between pages.
Navigation Actions
ViewModels have access to protected navigation methods wired up by the framework:
Navigate
Navigate directly by path:
csharp
Navigate("/"); // Go to home
Navigate("/settings"); // Go to settings
Navigate("/items/42"); // Go to item 421
2
3
2
3
NavigateWithParams
Navigate using a template with parameters:
csharp
NavigateWithParams("/items/{id}", new { id = 42 });
NavigateWithParams("/users/{name}", new { name = "alice" });1
2
2
Shutdown
Request graceful application shutdown:
csharp
Shutdown(); // Exits the application1
Navigation Example
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);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Navigation Behavior
When registering routes, you can control how navigation behaves:
csharp
termina.RegisterRoute<DetailPage, DetailViewModel>(
"/items/{id}",
NavigationBehavior.PreserveState); // Keep ViewModel state1
2
3
2
3
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 subscriptionsOnActivated()recreates subscriptions
- Layout lifecycle:
OnDeactivate()pauses timers, stops animations, pauses observable subscriptionsOnActivate()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();
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
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();
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
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
}
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
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<IInputEvent> input)
/// {
/// input.OfType<KeyPressed>()
/// .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;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174