Restructured and expanded the migration guide to provide a comprehensive resource for transitioning from Terminal.Gui v1 to v2. Key updates include: - Added a Table of Contents for easier navigation. - Summarized major architectural changes in v2, including the instance-based application model, IRunnable architecture, and 24-bit TrueColor support. - Updated examples to reflect new patterns, such as initializers replacing constructors and explicit disposal using `IDisposable`. - Documented changes to the layout system, including the removal of `Absolute`/`Computed` styles and the introduction of `Viewport`. - Standardized event patterns to use `object sender, EventArgs args`. - Detailed updates to the Keyboard, Mouse, and Navigation APIs, including configurable key bindings and viewport-relative mouse coordinates. - Replaced legacy components like `ScrollView` and `ContextMenu` with built-in scrolling and `PopoverMenu`. - Clarified disposal rules and introduced best practices for resource management. - Provided a complete migration example and a summary of breaking changes. This update aims to simplify the migration process by addressing breaking changes, introducing new features, and aligning with modern .NET conventions.
22 KiB
Migrating From v1 To v2
This document provides a comprehensive guide for migrating applications from Terminal.Gui v1 to v2.
For detailed breaking change documentation, check out this Discussion: https://github.com/gui-cs/Terminal.Gui/discussions/2448
Table of Contents
- Overview of Major Changes
- Application Architecture
- View Construction and Initialization
- Layout System Changes
- Color and Attribute Changes
- Type Changes
- Unicode and Text
- Keyboard API
- Mouse API
- Navigation Changes
- Scrolling Changes
- Adornments
- Event Pattern Changes
- View-Specific Changes
- Disposal and Resource Management
- API Terminology Changes
Overview of Major Changes
Terminal.Gui v2 represents a major architectural evolution with these key improvements:
- Instance-Based Application Model - Move from static
ApplicationtoIApplicationinstances - IRunnable Architecture - Interface-based runnable pattern with type-safe results
- Simplified Layout - Removed Absolute/Computed distinction, improved adornments
- 24-bit TrueColor - Full color support by default
- Enhanced Input - Better keyboard and mouse APIs
- Built-in Scrolling - All views support scrolling inherently
- Fluent API - Method chaining for elegant code
- Proper Disposal - IDisposable pattern throughout
Application Architecture
Instance-Based Application Model
v1 Pattern (Static):
// v1 - static Application
Application.Init();
var top = Application.Top;
top.Add(myView);
Application.Run();
Application.Shutdown();
v2 Recommended Pattern (Instance-Based):
// v2 - instance-based with using statement
using (var app = Application.Create().Init())
{
var top = new Window();
top.Add(myView);
app.Run(top);
top.Dispose();
} // app.Dispose() called automatically
v2 Legacy Pattern (Still Works):
// v2 - legacy static (marked obsolete but functional)
Application.Init();
var top = new Window();
top.Add(myView);
Application.Run(top);
top.Dispose();
Application.Shutdown(); // Obsolete - use Dispose() instead
IRunnable Architecture
v2 introduces IRunnable<TResult> for type-safe, runnable views:
// Create a dialog that returns a typed result
public class FileDialog : Runnable<string?>
{
private TextField _pathField;
public FileDialog()
{
Title = "Select File";
_pathField = new TextField { Width = Dim.Fill() };
Add(_pathField);
var okButton = new Button { Text = "OK", IsDefault = true };
okButton.Accepting += (s, e) => {
Result = _pathField.Text;
Application.RequestStop();
};
AddButton(okButton);
}
protected override bool OnIsRunningChanging(bool oldValue, bool newValue)
{
if (!newValue) // Stopping - extract result before disposal
{
Result = _pathField?.Text;
}
return base.OnIsRunningChanging(oldValue, newValue);
}
}
// Use with fluent API
using (var app = Application.Create().Init())
{
app.Run<FileDialog>();
string? result = app.GetResult<string>();
if (result is { })
{
OpenFile(result);
}
}
Key Benefits:
- Type-safe results (no casting)
- Automatic disposal of framework-created runnables
- CWP-compliant lifecycle events
- Works with any View (not just Toplevel)
Disposal and Resource Management
v2 requires explicit disposal:
// ❌ v1 - Application.Shutdown() disposed everything
Application.Init();
var top = new Window();
Application.Run(top);
Application.Shutdown(); // Disposed top automatically
// ✅ v2 - Dispose views explicitly
using (var app = Application.Create().Init())
{
var top = new Window();
app.Run(top);
top.Dispose(); // Must dispose
}
// ✅ v2 - Framework-created runnables disposed automatically
using (var app = Application.Create().Init())
{
app.Run<MyDialog>(); // Dialog disposed automatically
var result = app.GetResult<MyResult>();
}
Disposal Rules:
- "Whoever creates it, owns it"
Run<TRunnable>(): Framework creates → Framework disposesRun(IRunnable): Caller creates → Caller disposes- Always dispose
IApplication(useusingstatement)
View.App Property
Views now have an App property for accessing the application context:
// ❌ v1 - Direct static reference
Application.Driver.Move(x, y);
// ✅ v2 - Use View.App
App?.Driver.Move(x, y);
// ✅ v2 - Dependency injection
public class MyView : View
{
private readonly IApplication _app;
public MyView(IApplication app)
{
_app = app;
}
}
View Construction and Initialization
Constructors → Initializers
v1:
var myView = new View(new Rect(10, 10, 40, 10));
v2:
var myView = new View
{
X = 10,
Y = 10,
Width = 40,
Height = 10
};
Initialization Pattern
v2 uses ISupportInitializeNotification:
// v1 - No explicit initialization
var view = new View();
Application.Run(view);
// v2 - Automatic initialization via BeginInit/EndInit
var view = new View();
// BeginInit() called automatically when added to SuperView
// EndInit() called automatically
// Initialized event raised after EndInit()
Layout System Changes
Removed LayoutStyle Distinction
v1 had Absolute and Computed layout styles. v2 removed this distinction.
v1:
view.LayoutStyle = LayoutStyle.Computed;
v2:
// No LayoutStyle - all layout is declarative via Pos/Dim
view.X = Pos.Center();
view.Y = Pos.Center();
view.Width = Dim.Percent(50);
view.Height = Dim.Fill();
Frame vs Bounds
v1:
Frame- Position/size in SuperView coordinatesBounds- Always{0, 0, Width, Height}(location always empty)
v2:
Frame- Position/size in SuperView coordinates (same as v1)Viewport- Visible area in content coordinates (replaces Bounds)- Important:
Viewport.Locationcan now be non-zero for scrolling
- Important:
// ❌ v1
var size = view.Bounds.Size;
Debug.Assert(view.Bounds.Location == Point.Empty); // Always true
// ✅ v2
var visibleArea = view.Viewport;
var contentSize = view.GetContentSize();
// Viewport.Location can be non-zero when scrolled
view.ScrollVertical(10);
Debug.Assert(view.Viewport.Location.Y == 10);
Pos and Dim API Changes
| v1 | v2 |
|---|---|
Pos.At(x) |
Pos.Absolute(x) |
Dim.Sized(width) |
Dim.Absolute(width) |
Pos.Anchor() |
Pos.GetAnchor() |
Dim.Anchor() |
Dim.GetAnchor() |
// ❌ v1
view.X = Pos.At(10);
view.Width = Dim.Sized(20);
// ✅ v2
view.X = Pos.Absolute(10);
view.Width = Dim.Absolute(20);
View.AutoSize Removed
v1:
view.AutoSize = true;
v2:
view.Width = Dim.Auto();
view.Height = Dim.Auto();
See Dim.Auto Deep Dive for details.
Adornments
v2 adds Border, Margin, and Padding as built-in adornments.
v1:
// Custom border drawing
view.Border = new Border { /* ... */ };
v2:
// Built-in Border adornment
view.BorderStyle = LineStyle.Single;
view.Border.Thickness = new Thickness(1);
view.Title = "My View";
// Built-in Margin and Padding
view.Margin.Thickness = new Thickness(2);
view.Padding.Thickness = new Thickness(1);
See Layout Deep Dive for complete details.
Color and Attribute Changes
24-bit TrueColor Default
v2 uses 24-bit color by default.
// v1 - Limited color palette
var color = Color.Brown;
// v2 - ANSI-compliant names + TrueColor
var color = Color.Yellow; // Brown renamed
var customColor = new Color(0xFF, 0x99, 0x00); // 24-bit RGB
Attribute.Make Removed
v1:
var attr = Attribute.Make(Color.BrightMagenta, Color.Blue);
v2:
var attr = new Attribute(Color.BrightMagenta, Color.Blue);
Color Name Changes
| v1 | v2 |
|---|---|
Color.Brown |
Color.Yellow |
Type Changes
Low-Level Types
| v1 | v2 |
|---|---|
Rect |
Rectangle |
Point |
Point |
Size |
Size |
// ❌ v1
Rect rect = new Rect(0, 0, 10, 10);
// ✅ v2
Rectangle rect = new Rectangle(0, 0, 10, 10);
Unicode and Text
NStack.ustring Removed
v1:
using NStack;
ustring text = "Hello";
var width = text.Sum(c => Rune.ColumnWidth(c));
v2:
using System.Text;
string text = "Hello";
var width = text.GetColumns(); // Extension method
Rune Changes
v1:
// Implicit cast
myView.AddRune(col, row, '▄');
// Width
var width = Rune.ColumnWidth(rune);
v2:
// Explicit constructor
myView.AddRune(col, row, new Rune('▄'));
// Width
var width = rune.GetColumns();
See Unicode for details.
Keyboard API
v2 has a completely redesigned keyboard API.
Key Class
v1:
KeyEvent keyEvent;
if (keyEvent.KeyCode == KeyCode.Enter) { }
v2:
Key key;
if (key == Key.Enter) { }
// Modifiers
if (key.Shift) { }
if (key.Ctrl) { }
// With modifiers
Key ctrlC = Key.C.WithCtrl;
Key shiftF1 = Key.F1.WithShift;
Key Bindings
v1:
// Override OnKeyPress
protected override bool OnKeyPress(KeyEvent keyEvent)
{
if (keyEvent.KeyCode == KeyCode.Enter)
{
// Handle
return true;
}
return base.OnKeyPress(keyEvent);
}
v2:
// Use KeyBindings + Commands
AddCommand(Command.Accept, HandleAccept);
KeyBindings.Add(Key.Enter, Command.Accept);
private bool HandleAccept()
{
// Handle
return true;
}
Application-Wide Keys
v1:
// Hard-coded Ctrl+Q
if (keyEvent.Key == Key.CtrlMask | Key.Q)
{
Application.RequestStop();
}
v2:
// Configurable quit key
if (key == Application.QuitKey)
{
Application.RequestStop();
}
// Change the quit key
Application.QuitKey = Key.Esc;
Navigation Keys
v2 has consistent, configurable navigation keys:
| Key | Purpose |
|---|---|
Tab |
Next TabStop |
Shift+Tab |
Previous TabStop |
F6 |
Next TabGroup |
Shift+F6 |
Previous TabGroup |
// Configurable
Application.NextTabStopKey = Key.Tab;
Application.PrevTabStopKey = Key.Tab.WithShift;
Application.NextTabGroupKey = Key.F6;
Application.PrevTabGroupKey = Key.F6.WithShift;
See Keyboard Deep Dive for complete details.
Mouse API
MouseEventEventArgs → MouseEventArgs
v1:
void HandleMouse(MouseEventEventArgs args) { }
v2:
void HandleMouse(object? sender, MouseEventArgs args) { }
Mouse Coordinates
v1:
- Mouse coordinates were screen-relative
v2:
- Mouse coordinates are now Viewport-relative
// v2 - Viewport-relative coordinates
view.MouseClick += (s, e) =>
{
// e.Position is relative to view's Viewport
var x = e.Position.X; // 0 = left edge of viewport
var y = e.Position.Y; // 0 = top edge of viewport
};
Highlight Event
v2 adds a Highlight event for visual feedback:
view.Highlight += (s, e) =>
{
// Provide visual feedback on mouse hover
};
view.HighlightStyle = HighlightStyle.Hover;
See Mouse Deep Dive for complete details.
Navigation Changes
Focus Properties
v1:
view.CanFocus = true; // Default was true
v2:
view.CanFocus = true; // Default is FALSE - must opt-in
Important: In v2, CanFocus defaults to false. Views that want focus must explicitly set it.
Focus Changes
v1:
// HasFocus was read-only
bool hasFocus = view.HasFocus;
v2:
// HasFocus can be set
view.HasFocus = true; // Equivalent to SetFocus()
view.HasFocus = false; // Equivalent to SuperView.AdvanceFocus()
TabStop Behavior
v1:
view.TabStop = true; // Boolean
v2:
view.TabStop = TabBehavior.TabStop; // Enum with more options
// Options:
// - NoStop: Focusable but not via Tab
// - TabStop: Normal tab navigation
// - TabGroup: Advance via F6
Navigation Events
v1:
view.Enter += (s, e) => { }; // Gained focus
view.Leave += (s, e) => { }; // Lost focus
v2:
view.HasFocusChanging += (s, e) =>
{
// Before focus changes (cancellable)
if (preventFocusChange)
e.Cancel = true;
};
view.HasFocusChanged += (s, e) =>
{
// After focus changed
if (e.Value)
Console.WriteLine("Gained focus");
else
Console.WriteLine("Lost focus");
};
See Navigation Deep Dive for complete details.
Scrolling Changes
ScrollView Removed
v1:
var scrollView = new ScrollView
{
ContentSize = new Size(100, 100),
ShowHorizontalScrollIndicator = true,
ShowVerticalScrollIndicator = true
};
v2:
// Built-in scrolling on every View
var view = new View();
view.SetContentSize(new Size(100, 100));
// Built-in scrollbars
view.VerticalScrollBar.Visible = true;
view.HorizontalScrollBar.Visible = true;
view.VerticalScrollBar.AutoShow = true;
Scrolling API
v2:
// Set content larger than viewport
view.SetContentSize(new Size(100, 100));
// Scroll by changing Viewport location
view.Viewport = view.Viewport with { Location = new Point(10, 10) };
// Or use helper methods
view.ScrollVertical(5);
view.ScrollHorizontal(3);
See Scrolling Deep Dive for complete details.
Event Pattern Changes
v2 standardizes all events to use object sender, EventArgs args pattern.
Button.Clicked → Button.Accepting
v1:
button.Clicked += () => { /* do something */ };
v2:
button.Accepting += (s, e) => { /* do something */ };
Event Signatures
v1:
// Various patterns
event Action SomeEvent;
event Action<string> OtherEvent;
event Action<EventArgs> ThirdEvent;
v2:
// Consistent pattern
event EventHandler<EventArgs>? SomeEvent;
event EventHandler<EventArgs<string>>? OtherEvent;
event EventHandler<CancelEventArgs<bool>>? ThirdEvent;
Benefits:
- Named parameters
- Cancellable events via
CancelEventArgs - Future-proof (new properties can be added)
View-Specific Changes
CheckBox
v1:
var cb = new CheckBox("_Checkbox", true);
cb.Toggled += (e) => { };
cb.Toggle();
v2:
var cb = new CheckBox
{
Title = "_Checkbox",
CheckState = CheckState.Checked
};
cb.CheckStateChanging += (s, e) =>
{
e.Cancel = preventChange;
};
cb.AdvanceCheckState();
StatusBar
v1:
var statusBar = new StatusBar(
new StatusItem[]
{
new StatusItem(Application.QuitKey, "Quit", () => Quit())
}
);
v2:
var statusBar = new StatusBar(
new Shortcut[]
{
new Shortcut(Application.QuitKey, "Quit", Quit)
}
);
PopoverMenu
v2 replaces ContextMenu with PopoverMenu:
v1:
var contextMenu = new ContextMenu();
v2:
var popoverMenu = new PopoverMenu();
MenuItem
v1:
new MenuItem(
"Copy",
"",
CopyGlyph,
null,
null,
(KeyCode)Key.G.WithCtrl
)
v2:
new MenuItem(
"Copy",
"",
CopyGlyph,
Key.G.WithCtrl
)
Disposal and Resource Management
v2 implements proper IDisposable throughout.
View Disposal
// v1 - No explicit disposal needed
var view = new View();
Application.Run(view);
Application.Shutdown();
// v2 - Explicit disposal required
var view = new View();
app.Run(view);
view.Dispose();
app.Dispose();
Disposal Patterns
// ✅ Best practice - using statement
using (var app = Application.Create().Init())
{
using (var view = new View())
{
app.Run(view);
}
}
// ✅ Alternative - explicit try/finally
var app = Application.Create();
try
{
app.Init();
var view = new View();
try
{
app.Run(view);
}
finally
{
view.Dispose();
}
}
finally
{
app.Dispose();
}
SubView Disposal
When a View is disposed, it automatically disposes all SubViews:
var container = new View();
var child1 = new View();
var child2 = new View();
container.Add(child1, child2);
// Disposes container, child1, and child2
container.Dispose();
See Resource Management for complete details.
API Terminology Changes
v2 modernizes terminology for clarity:
Application.Top → Application.TopRunnable
v1:
Application.Top.SetNeedsDraw();
v2:
// Use TopRunnable (or TopRunnableView for View reference)
app.TopRunnable?.SetNeedsDraw();
app.TopRunnableView?.SetNeedsDraw();
// From within a view
App?.TopRunnableView?.SetNeedsDraw();
Why "TopRunnable"?
- Clearly indicates it's the top of the runnable session stack
- Aligns with
IRunnablearchitecture - Works with any
IRunnable, not justToplevel
Application.TopLevels → Application.SessionStack
v1:
foreach (var tl in Application.TopLevels)
{
// Process
}
v2:
foreach (var token in app.SessionStack)
{
var runnable = token.Runnable;
// Process
}
// Count of sessions
int sessionCount = app.SessionStack.Count;
Why "SessionStack"?
- Describes both content (sessions) and structure (stack)
- Aligns with
SessionTokenterminology - Follows .NET naming patterns
View Arrangement
v1:
view.SendSubViewToBack();
view.SendSubViewBackward();
view.SendSubViewToFront();
view.SendSubViewForward();
v2:
// Fixed naming (methods worked opposite to their names in v1)
view.MoveSubViewToStart();
view.MoveSubViewTowardsStart();
view.MoveSubViewToEnd();
view.MoveSubViewTowardsEnd();
Mdi → ViewArrangement.Overlapped
v1:
Application.MdiTop = true;
toplevel.IsMdiContainer = true;
v2:
view.Arrangement = ViewArrangement.Overlapped;
// Additional flags
view.Arrangement = ViewArrangement.Movable | ViewArrangement.Resizable;
See Arrangement Deep Dive for complete details.
Complete Migration Example
Here's a complete v1 to v2 migration:
v1:
using NStack;
using Terminal.Gui;
Application.Init();
var win = new Window(new Rect(0, 0, 50, 20), "Hello");
var label = new Label(1, 1, "Name:");
var textField = new TextField(10, 1, 30, "");
var button = new Button(10, 3, "OK");
button.Clicked += () =>
{
MessageBox.Query(50, 7, "Info", $"Hello, {textField.Text}", "Ok");
};
win.Add(label, textField, button);
Application.Top.Add(win);
Application.Run();
Application.Shutdown();
v2:
using System;
using Terminal.Gui;
using (var app = Application.Create().Init())
{
var win = new Window
{
Title = "Hello",
Width = 50,
Height = 20
};
var label = new Label
{
Text = "Name:",
X = 1,
Y = 1
};
var textField = new TextField
{
X = 10,
Y = 1,
Width = 30
};
var button = new Button
{
Text = "OK",
X = 10,
Y = 3
};
button.Accepting += (s, e) =>
{
MessageBox.Query(app, "Info", $"Hello, {textField.Text}", "Ok");
};
win.Add(label, textField, button);
app.Run(win);
win.Dispose();
}
Summary of Major Breaking Changes
| Category | v1 | v2 |
|---|---|---|
| Application | Static Application |
IApplication instances via Application.Create() |
| Disposal | Automatic | Explicit (IDisposable pattern) |
| View Construction | Constructors with Rect | Initializers with X, Y, Width, Height |
| Layout | Absolute/Computed distinction | Unified Pos/Dim system |
| Colors | Limited palette | 24-bit TrueColor default |
| Types | Rect, NStack.ustring |
Rectangle, System.String |
| Keyboard | KeyEvent, hard-coded keys |
Key, configurable bindings |
| Mouse | Screen-relative | Viewport-relative |
| Scrolling | ScrollView |
Built-in on all Views |
| Focus | CanFocus default true |
CanFocus default false |
| Navigation | Enter/Leave events |
HasFocusChanging/HasFocusChanged |
| Events | Mixed patterns | Standard EventHandler<EventArgs> |
| Terminology | Application.Top, TopLevels |
TopRunnable, SessionStack |
Additional Resources
- Application Deep Dive - Complete application architecture
- View Deep Dive - View system details
- Layout Deep Dive - Comprehensive layout guide
- Keyboard Deep Dive - Keyboard input handling
- Mouse Deep Dive - Mouse input handling
- Navigation Deep Dive - Focus and navigation
- Scrolling Deep Dive - Built-in scrolling system
- Arrangement Deep Dive - Movable/resizable views
- Configuration Deep Dive - Configuration system
- What's New in v2 - New features overview