diff --git a/Examples/FluentExample/FluentExample.csproj b/Examples/FluentExample/FluentExample.csproj
new file mode 100644
index 000000000..2086af6ed
--- /dev/null
+++ b/Examples/FluentExample/FluentExample.csproj
@@ -0,0 +1,11 @@
+
+
+ Exe
+ net8.0
+ preview
+ enable
+
+
+
+
+
diff --git a/Examples/FluentExample/Program.cs b/Examples/FluentExample/Program.cs
new file mode 100644
index 000000000..e27caf26e
--- /dev/null
+++ b/Examples/FluentExample/Program.cs
@@ -0,0 +1,143 @@
+// Fluent API example demonstrating IRunnable with automatic disposal and result extraction
+
+using Terminal.Gui.App;
+using Terminal.Gui.Drawing;
+using Terminal.Gui.ViewBase;
+using Terminal.Gui.Views;
+
+#if POST_4148
+// Run the application with fluent API - automatically creates, runs, and disposes the runnable
+
+// Display the result
+if (Application.Create ()
+ .Init ()
+ .Run ()
+ .Shutdown () is Color { } result)
+{
+ Console.WriteLine (@$"Selected Color: {(Color?)result}");
+}
+else
+{
+ Console.WriteLine (@"No color selected");
+}
+#else
+
+// Run using traditional approach
+IApplication app = Application.Create ();
+app.Init ();
+var colorPicker = new ColorPickerView ();
+app.Run (colorPicker);
+
+Color? resultColor = colorPicker.Result;
+
+colorPicker.Dispose ();
+app.Shutdown ();
+
+if (resultColor is { } result)
+{
+ Console.WriteLine (@$"Selected Color: {(Color?)result}");
+}
+else
+{
+ Console.WriteLine (@"No color selected");
+}
+
+#endif
+
+#if POST_4148
+///
+/// A runnable view that allows the user to select a color.
+/// Demonstrates IRunnable pattern with automatic disposal.
+///
+public class ColorPickerView : Runnable
+{
+
+#else
+///
+/// A runnable view that allows the user to select a color.
+/// Uses the traditional approach without automatic disposal/Fluent API.
+///
+public class ColorPickerView : Toplevel
+{
+ public Color? Result { get; set; }
+
+#endif
+ public ColorPickerView ()
+ {
+ Title = "Select a Color (Esc to quit)";
+ BorderStyle = LineStyle.Single;
+ Height = Dim.Auto ();
+ Width = Dim.Auto ();
+
+ // Add instructions
+ var instructions = new Label
+ {
+ Text = "Use arrow keys to select a color, Enter to accept",
+ X = Pos.Center (),
+ Y = 0
+ };
+
+ // Create color picker
+ ColorPicker colorPicker = new ()
+ {
+ X = Pos.Center (),
+ Y = Pos.Bottom (instructions),
+ Style = new ColorPickerStyle ()
+ {
+ ShowColorName = true,
+ ShowTextFields = true
+ }
+ };
+ colorPicker.ApplyStyleChanges ();
+
+ // Create OK button
+ Button okButton = new ()
+ {
+ Title = "_OK",
+ X = Pos.Align (Alignment.Center),
+ Y = Pos.AnchorEnd (),
+ IsDefault = true
+ };
+
+ okButton.Accepting += (s, e) =>
+ {
+ // Extract result before stopping
+ Result = colorPicker.SelectedColor;
+ RequestStop ();
+ e.Handled = true;
+ };
+
+ // Create Cancel button
+ Button cancelButton = new ()
+ {
+ Title = "_Cancel",
+ X = Pos.Align (Alignment.Center),
+ Y = Pos.AnchorEnd ()
+ };
+
+ cancelButton.Accepting += (s, e) =>
+ {
+ // Don't set result - leave as null
+ RequestStop ();
+ e.Handled = true;
+ };
+
+ // Add views
+ Add (instructions, colorPicker, okButton, cancelButton);
+ }
+
+#if POST_4148
+ protected override bool OnIsRunningChanging (bool oldIsRunning, bool newIsRunning)
+ {
+ // Alternative place to extract result before stopping
+ // This is called before the view is removed from the stack
+ if (!newIsRunning && Result is null)
+ {
+ // User pressed Esc - could extract current selection here
+ // Result = _colorPicker.SelectedColor;
+ }
+
+ return base.OnIsRunningChanging (oldIsRunning, newIsRunning);
+ }
+#endif
+}
diff --git a/Examples/RunnableWrapperExample/Program.cs b/Examples/RunnableWrapperExample/Program.cs
new file mode 100644
index 000000000..41a2df32b
--- /dev/null
+++ b/Examples/RunnableWrapperExample/Program.cs
@@ -0,0 +1,165 @@
+// Example demonstrating how to make ANY View runnable without implementing IRunnable
+
+using Terminal.Gui.App;
+using Terminal.Gui.Drawing;
+using Terminal.Gui.ViewBase;
+using Terminal.Gui.Views;
+
+IApplication app = Application.Create ();
+app.Init ();
+
+// Example 1: Use extension method with result extraction
+var textField = new TextField { Width = 40, Text = "Default text" };
+textField.Title = "Enter your name";
+textField.BorderStyle = LineStyle.Single;
+
+var textRunnable = textField.AsRunnable (tf => tf.Text);
+app.Run (textRunnable);
+
+if (textRunnable.Result is { } name)
+{
+ MessageBox.Query ("Result", $"You entered: {name}", "OK");
+}
+else
+{
+ MessageBox.Query ("Result", "Canceled", "OK");
+}
+textRunnable.Dispose ();
+
+// Example 2: Use IApplication.RunView() for one-liner
+var selectedColor = app.RunView (
+ new ColorPicker
+ {
+ Title = "Pick a Color",
+ BorderStyle = LineStyle.Single
+ },
+ cp => cp.SelectedColor);
+
+MessageBox.Query ("Result", $"Selected color: {selectedColor}", "OK");
+
+// Example 3: FlagSelector with typed enum result
+var flagSelector = new FlagSelector
+{
+ Title = "Choose Styles",
+ BorderStyle = LineStyle.Single
+};
+
+var flagsRunnable = flagSelector.AsRunnable (fs => fs.Value);
+app.Run (flagsRunnable);
+
+MessageBox.Query ("Result", $"Selected styles: {flagsRunnable.Result}", "OK");
+flagsRunnable.Dispose ();
+
+// Example 4: Any View without result extraction
+var label = new Label
+{
+ Text = "Press Esc to continue...",
+ X = Pos.Center (),
+ Y = Pos.Center ()
+};
+
+var labelRunnable = label.AsRunnable ();
+app.Run (labelRunnable);
+
+// Can still access the wrapped view
+MessageBox.Query ("Result", $"Label text was: {labelRunnable.WrappedView.Text}", "OK");
+labelRunnable.Dispose ();
+
+// Example 5: Complex custom View made runnable
+var formView = CreateCustomForm ();
+var formRunnable = formView.AsRunnable (ExtractFormData);
+
+app.Run (formRunnable);
+
+if (formRunnable.Result is { } formData)
+{
+ MessageBox.Query (
+ "Form Results",
+ $"Name: {formData.Name}\nAge: {formData.Age}\nAgreed: {formData.Agreed}",
+ "OK");
+}
+formRunnable.Dispose ();
+
+app.Shutdown ();
+
+// Helper method to create a custom form
+View CreateCustomForm ()
+{
+ var form = new View
+ {
+ Title = "User Information",
+ BorderStyle = LineStyle.Single,
+ Width = 50,
+ Height = 10
+ };
+
+ var nameField = new TextField
+ {
+ Id = "nameField",
+ X = 10,
+ Y = 1,
+ Width = 30
+ };
+
+ var ageField = new TextField
+ {
+ Id = "ageField",
+ X = 10,
+ Y = 3,
+ Width = 10
+ };
+
+ var agreeCheckbox = new CheckBox
+ {
+ Id = "agreeCheckbox",
+ Title = "I agree to terms",
+ X = 10,
+ Y = 5
+ };
+
+ var okButton = new Button
+ {
+ Title = "OK",
+ X = Pos.Center (),
+ Y = 7,
+ IsDefault = true
+ };
+
+ okButton.Accepting += (s, e) =>
+ {
+ form.App?.RequestStop ();
+ e.Handled = true;
+ };
+
+ form.Add (new Label { Text = "Name:", X = 2, Y = 1 });
+ form.Add (nameField);
+ form.Add (new Label { Text = "Age:", X = 2, Y = 3 });
+ form.Add (ageField);
+ form.Add (agreeCheckbox);
+ form.Add (okButton);
+
+ return form;
+}
+
+// Helper method to extract data from the custom form
+FormData ExtractFormData (View form)
+{
+ var nameField = form.SubViews.FirstOrDefault (v => v.Id == "nameField") as TextField;
+ var ageField = form.SubViews.FirstOrDefault (v => v.Id == "ageField") as TextField;
+ var agreeCheckbox = form.SubViews.FirstOrDefault (v => v.Id == "agreeCheckbox") as CheckBox;
+
+ return new FormData
+ {
+ Name = nameField?.Text ?? string.Empty,
+ Age = int.TryParse (ageField?.Text, out int age) ? age : 0,
+ Agreed = agreeCheckbox?.CheckedState == CheckState.Checked
+ };
+}
+
+// Result type for custom form
+record FormData
+{
+ public string Name { get; init; } = string.Empty;
+ public int Age { get; init; }
+ public bool Agreed { get; init; }
+}
diff --git a/Examples/RunnableWrapperExample/RunnableWrapperExample.csproj b/Examples/RunnableWrapperExample/RunnableWrapperExample.csproj
new file mode 100644
index 000000000..7e34acedb
--- /dev/null
+++ b/Examples/RunnableWrapperExample/RunnableWrapperExample.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ latest
+
+
+
+
+
+
+
diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/AllViewsView.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/AllViewsView.cs
index 862cc2083..f8f78ac6f 100644
--- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/AllViewsView.cs
+++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/AllViewsView.cs
@@ -4,6 +4,7 @@ namespace UICatalog.Scenarios;
public class AllViewsView : View
{
private const int MAX_VIEW_FRAME_HEIGHT = 25;
+
public AllViewsView ()
{
CanFocus = true;
@@ -24,6 +25,7 @@ public class AllViewsView : View
AddCommand (Command.Down, () => ScrollVertical (1));
AddCommand (Command.PageUp, () => ScrollVertical (-SubViews.OfType ().First ().Frame.Height));
AddCommand (Command.PageDown, () => ScrollVertical (SubViews.OfType ().First ().Frame.Height));
+
AddCommand (
Command.Start,
() =>
@@ -32,6 +34,7 @@ public class AllViewsView : View
return true;
});
+
AddCommand (
Command.End,
() =>
@@ -65,12 +68,12 @@ public class AllViewsView : View
MouseBindings.Add (MouseFlags.WheeledRight, Command.ScrollRight);
}
- ///
+ ///
public override void EndInit ()
{
base.EndInit ();
- var allClasses = GetAllViewClassesCollection ();
+ List allClasses = GetAllViewClassesCollection ();
View? previousView = null;
@@ -95,19 +98,6 @@ public class AllViewsView : View
}
}
- private static List GetAllViewClassesCollection ()
- {
- List types = typeof (View).Assembly.GetTypes ()
- .Where (
- myType => myType is { IsClass: true, IsAbstract: false, IsPublic: true }
- && myType.IsSubclassOf (typeof (View)))
- .ToList ();
-
- types.Add (typeof (View));
-
- return types;
- }
-
private View? CreateView (Type type)
{
// If we are to create a generic Type
@@ -125,12 +115,32 @@ public class AllViewsView : View
}
else
{
- typeArguments.Add (typeof (object));
+ // Check if the generic parameter has constraints
+ Type [] constraints = arg.GetGenericParameterConstraints ();
+
+ if (constraints.Length > 0)
+ {
+ // Use the first constraint type to satisfy the constraint
+ typeArguments.Add (constraints [0]);
+ }
+ else
+ {
+ typeArguments.Add (typeof (object));
+ }
}
}
// And change what type we are instantiating from MyClass to MyClass