Stanza is a small, reflection-free MVVM binding library for Terminal.Gui v2. It uses C# Source Generators to eliminate boilerplate, providing a declarative way to wire up UI controls to ViewModels.
- Reflection-free enables NativeAOT compilation
- No framework dependencies: extensions are dependent on standard .NET APIs;
INotifyPropertyChanged,ICommandmeaning CommunityToolkit.Mvvm or Reactive Extensions can be used. - Automatic binding management or manual management by opting out of source generator usage
Add the package to your project:
dotnet add package Stanza.TerminalGuiUsing CommunityToolkit.Mvvm (optional but recommended) to handle property notifications.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
public partial string Username { get; set; } = "Stanza User";
[RelayCommand]
private void Reset() => Username = "Guest";
}Declare your UI using standard Terminal.Gui code, and use Stanza attributes to bind the data.
using Stanza.TerminalGui;
using Terminal.Gui.Views;
// 1. Mark as partial
// 2. Use the generic StanzaView attribute to specify your ViewModel
[StanzaView<MainViewModel>]
public partial class MainView : Window
{
// Bind the Text property of this Label to ViewModel.Username
[BindText(nameof(MainViewModel.Username))]
public Label NameLabel { get; set; } = new();
// Bind the Button trigger to the ResetCommand
[BindCommand(nameof(MainViewModel.ResetCommand))]
public Button ResetButton { get; set; } = new() { Text = "Reset" };
public MainView()
{
Title = "Stanza Quickstart";
ResetButton.Y = Pos.Bottom(NameLabel);
Add(NameLabel, ResetButton);
}
}Simply assign the ViewModel property. Stanza handles the subscription logic automatically.
var viewModel = new MainViewModel();
var view = new MainView { ViewModel = viewModel };
Application.Run(view);| Attribute | Target Property | Support |
|---|---|---|
[BindText] |
Text |
Two-Way (TextField), One-Way (Label) |
[BindChecked] |
Checked / Value |
Two-Way (CheckBox) |
[BindEnabled] |
Enabled |
One-Way |
[BindVisible] |
Visible |
One-Way |
[BindCommand] |
Accepting / Enabled |
Execute + CanExecute (Button) |
If you need logic that doesn't fit a standard attribute, you can use the OnApplyBindings hook provided by the generator. This method is called automatically whenever a new ViewModel is attached.
public partial class MainView
{
// This partial method is called by the generated code
partial void OnApplyBindings(BindingContext context)
{
// Manual binding with custom logic
this.Bind(ViewModel, vm => vm.Username, val => {
Title = $"Editing: {val}";
}).AddTo(context);
// Context is automatically disposed when the VM changes
// or the View is disposed.
}
}In standard Terminal.Gui, you often find yourself writing code like this:
// The "Old" Way (Manual, Leak-prone, No Thread Safety)
viewModel.PropertyChanged += (s, e) => {
if (e.PropertyName == "Name") {
Application.Invoke(() => label.Text = viewModel.Name);
}
};Stanza replaces this with a single attribute. It handles the property name check, the thread marshalling, the initial synchronization, and the event unsubscription for you.