Ver código fonte

Updates `IListDataSource.Render` to rename the `start` parameter to `viewportXOffset` (#4392)

* Add comprehensive unit tests for WindowsKeyConverter

- Implement 118 parallelizable unit tests for WindowsKeyConverter

- Cover ToKey and ToKeyInfo methods with full bidirectional testing

- Test basic characters, modifiers, special keys, function keys

- Test VK_PACKET Unicode/IME input

- Test OEM keys, NumPad keys, and lock states

- Include round-trip conversion tests

- All tests passing successfully

Fixes #4389

* Rename `start` parameter to `viewportXOffset` for clarity

The `start` parameter in several methods and interfaces has been
renamed to `viewportXOffset` to better reflect its purpose as the
horizontal offset of the viewport during string rendering.

- Updated method signatures in `ListViewWithSelection` to use
  `viewportXOffset` instead of `start`, including default values.
- Modified the `RenderUstr` method in `ListViewWithSelection` to
  use `viewportXOffset` for calculating the starting index.
- Renamed the `start` parameter to `viewportXOffset` in the
  `IListDataSource` interface and updated its documentation.
- Replaced all occurrences of `start` with `viewportXOffset` in
  the `ListWrapper<T>` class, including method calls and logic.
- Updated the `RenderUstr` method in `ListWrapper<T>` to use
  `viewportXOffset` for substring calculations.
- Adjusted the test method in `ListViewTests.cs` to reflect the
  parameter name change.

These changes improve code readability and make the parameter's
role in rendering logic more explicit.

* Remove WindowsKeyConverterTests class that was added by mistake

* Modernized ListView and IListDataSource - Tons of new unit tests

Refactored `ListView` and `IListDataSource` to improve readability, maintainability, and functionality. Introduced `ListWrapper<T>` as a default implementation of `IListDataSource` for easier integration with standard collections.

Enhanced `ListView` with better handling of marking, selection, and scrolling. Replaced `viewportXOffset` with `viewportX` for horizontal scrolling. Added `EnsureSelectedItemVisible` to maintain visibility of the selected item.

Updated `IListDataSource` with detailed XML documentation and added `SuspendCollectionChangedEvent` for bulk updates. Improved null safety with nullable reference types.

Added comprehensive unit tests for `ListWrapper<T>` and `IListDataSource` to ensure robustness. Modernized the codebase with C# features like expression-bodied members and pattern matching. Fixed bugs related to `SelectedItem` validation and rendering artifacts.

* Improve index validation in ComboBox and ListView

Enhance robustness by adding stricter checks for valid indices
in ComboBox and ListView. Updated conditions in the
`_listview.SelectedItemChanged` event handler to ensure `e.Item`
is non-negative before accessing `_searchSet`. Modified the
`SetValue` method to use `e.Item` instead of `_listview.SelectedItem`.

In ListView, updated the `OnSelectedChanged` method to validate
that `SelectedItem` is non-negative (`>= 0`) before accessing
the `Source` list. These changes prevent potential out-of-range
errors and improve code safety.

* Refactor and enhance test coverage across modules

Refactored and added new tests to improve coverage, readability, and consistency across multiple test files. Key changes include:

- **ShortcutTests.cs**: Added tests for `BindKeyToApplication` and removed redundant tests.
- **SourcesManagerTests.cs**: Renamed `Update_*` tests to `Load_*` for clarity.
- **ArrangementTests.cs**: Reintroduced `MouseGrabHandler` tests, added `ViewArrangement` flag tests, and improved structure.
- **NeedsDrawTests.cs**: Replaced `Application.Screen.Size` with fixed dimensions for better isolation.
- **DimAutoTests.cs**: Updated layout tests to use fixed dimensions.
- **FrameTests.cs**: Standardized object initialization and validated frame behavior.
- **SubViewTests.cs**: Improved formatting and modernized event handling.
- **NumericUpDownTests.cs**: Decoupled layout tests from screen size.

General improvements:
- Enhanced formatting and removed redundant tests.
- Added comments for clarity.
- Introduced `ITestOutputHelper` for better debugging in `ArrangementTests`.

* Refactor to use nullable types for better null safety

Enabled nullable reference types across the codebase to improve null safety and prevent potential null reference issues. Refactored `SelectedItem` and related properties from `int` to `int?` to represent no selection with `null` instead of `-1`. Updated logic, event arguments, and method signatures to handle nullable values consistently.

Simplified object initialization using modern C# syntax and improved code readability with interpolated strings. Added null checks and early returns to prevent runtime errors. Enhanced error handling by throwing `ArgumentOutOfRangeException` for invalid values.

Updated tests to reflect the changes, replacing assertions for `-1` with `null` and ensuring proper handling of nullable values. Cleaned up redundant code and improved formatting for better maintainability.

* on` functionality has been deprecated, refactored, or removed from the `Shortcut` class.

* Refactor: Transition to instance-based architecture

Updated `Run-LocalCoverage.ps1` to increase `--blame-hang-timeout` from 10s to 60s. Improved null safety in `GuiTestContext` by adding null-conditional operators. Commented out problematic code in `SetupFakeApplicationAttribute.cs` to prevent test hangs.

Excluded `ViewBase` files from `UnitTests.Parallelizable.csproj` and removed redundant folder declarations. Simplified event handling in `IListDataSourceTests.cs` and updated `ListViewTests.cs` to use nullable reference types.

Enhanced documentation to emphasize the transition to an instance-based application architecture. Updated examples in `application.md`, `multitasking.md`, and `navigation.md` to reflect the use of `Application.Create()` and `View.App`. Clarified the obsolescence of the static `Application` class.

Revised table of contents in `toc.yml` to include new sections like "Application Deep Dive" and "Scheme Deep Dive." Added `dotnet-tools.json` for tool configuration.

These changes improve maintainability, testability, and alignment with modern C# practices.

* Refactor ListViewTests to use Terminal.Gui framework

The `ListViewTests` class has been refactored to replace the `AutoInitShutdown` attribute with explicit application lifecycle management using `IApplication` and `app.Init()` from the `Terminal.Gui` framework.

Key changes include:
- Rewriting tests to use `Terminal.Gui`'s application lifecycle.
- Adding a private `_output` field for logging test output via `ITestOutputHelper`.
- Updating `DriverAssert.AssertDriverContentsWithFrameAre` to include `app.Driver` for UI verification.
- Rewriting tests like `Clicking_On_Border_Is_Ignored`, `EnsureSelectedItemVisible_SelectedItem`, and others to align with the new framework.
- Adding explicit calls to `app.Shutdown()` for proper cleanup.
- Enabling nullable reference types with `#nullable enable`.
- Updating `using` directives and `namespace` to reflect the new structure.

These changes improve test maintainability, compatibility, and diagnostics.

* Update Terminal.Gui/Views/CollectionNavigation/CollectionNavigatorBase.cs

Co-authored-by: Copilot <[email protected]>

* Update Terminal.Gui/Views/CollectionNavigation/CollectionNavigatorBase.cs

Co-authored-by: Copilot <[email protected]>

* Update Examples/UICatalog/UICatalogTop.cs

Co-authored-by: Copilot <[email protected]>

* Update Terminal.Gui/Views/ListWrapper.cs

Co-authored-by: Copilot <[email protected]>

* Update Terminal.Gui/Views/ListWrapper.cs

Co-authored-by: Copilot <[email protected]>

* Updated the `SetMark` method to return `Source.IsMarked(SelectedItem.Value)` for consistency and removed an outdated comment questioning its correctness.

Enhanced the exception message in the `SelectedItem` property setter to provide clearer guidance when the value is out of range.

* Add comprehensive ListView behavior test coverage

Added multiple test methods to validate `ListView` behavior:
- `Vertical_ScrollBar_Hides_And_Shows_As_Needed`: Ensures the vertical scrollbar auto-hides/shows based on content height.
- `Mouse_Wheel_Scrolls`: Verifies vertical scrolling with the mouse wheel updates `TopItem`.
- `SelectedItem_With_Source_Null_Does_Nothing`: Confirms no exceptions occur when setting `SelectedItem` with a `null` source.
- `Horizontal_Scroll`: Tests horizontal scrolling, including programmatic and mouse wheel interactions, ensuring `LeftItem` updates correctly.
- `SetSourceAsync_SetsSource`: Validates the asynchronous `SetSourceAsync` method updates the source and item count.
- `AllowsMultipleSelection_Set_To_False_Unmarks_All_But_Selected`: Ensures disabling multiple selection unmarks all but the selected item.
- `Source_CollectionChanged_Remove`: Confirms `SelectedItem` and source count update correctly when items are removed from the source collection.

---------

Co-authored-by: Copilot <[email protected]>
Tig 3 semanas atrás
pai
commit
a6258ed398
44 arquivos alterados com 3003 adições e 2008 exclusões
  1. 5 0
      .config/dotnet-tools.json
  2. 1 1
      Examples/UICatalog/Scenarios/AllViewsTester.cs
  3. 2 2
      Examples/UICatalog/Scenarios/ComboBoxIteration.cs
  4. 12 12
      Examples/UICatalog/Scenarios/DynamicMenuBar.cs
  5. 21 8
      Examples/UICatalog/Scenarios/DynamicStatusBar.cs
  6. 4 4
      Examples/UICatalog/Scenarios/ListViewWithSelection.cs
  7. 1 1
      Examples/UICatalog/Scenarios/ListsAndCombos.cs
  8. 1 1
      Examples/UICatalog/Scenarios/SpinnerStyles.cs
  9. 11 3
      Examples/UICatalog/UICatalogTop.cs
  10. 1 1
      Scripts/Run-LocalCoverage.ps1
  11. 40 34
      Terminal.Gui/Views/CollectionNavigation/CollectionNavigatorBase.cs
  12. 4 4
      Terminal.Gui/Views/CollectionNavigation/ICollectionNavigator.cs
  13. 17 10
      Terminal.Gui/Views/ComboBox.cs
  14. 75 32
      Terminal.Gui/Views/IListDataSource.cs
  15. 234 517
      Terminal.Gui/Views/ListView.cs
  16. 4 5
      Terminal.Gui/Views/ListViewEventArgs.cs
  17. 256 0
      Terminal.Gui/Views/ListWrapper.cs
  18. 3 3
      Terminal.Gui/Views/TableView/TableView.cs
  19. 1 1
      Terminal.Gui/Views/Toplevel.cs
  20. 1 0
      Terminal.sln.DotSettings
  21. 2 2
      Tests/IntegrationTests/UICatalog/ScenarioTests.cs
  22. 1 1
      Tests/TerminalGuiFluentTesting/GuiTestContext.ContextMenu.cs
  23. 2 2
      Tests/TerminalGuiFluentTesting/GuiTestContext.ViewBase.cs
  24. 4 1
      Tests/UnitTests/SetupFakeApplicationAttribute.cs
  25. 0 1225
      Tests/UnitTests/Views/ListViewTests.cs
  26. 11 11
      Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs
  27. 15 15
      Tests/UnitTestsParallelizable/Text/CollectionNavigatorTests.cs
  28. 5 3
      Tests/UnitTestsParallelizable/UnitTests.Parallelizable.csproj
  29. 5 5
      Tests/UnitTestsParallelizable/View/Draw/NeedsDrawTests.cs
  30. 1 1
      Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.PosTypes.cs
  31. 7 7
      Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.cs
  32. 5 5
      Tests/UnitTestsParallelizable/View/Layout/FrameTests.cs
  33. 6 6
      Tests/UnitTestsParallelizable/View/SubviewTests.cs
  34. 513 0
      Tests/UnitTestsParallelizable/Views/IListDataSourceTests.cs
  35. 1502 38
      Tests/UnitTestsParallelizable/Views/ListViewTests.cs
  36. 5 5
      Tests/UnitTestsParallelizable/Views/NumericUpDownTests.cs
  37. 34 22
      docfx/docs/application.md
  38. 2 1
      docfx/docs/config.md
  39. 8 0
      docfx/docs/index.md
  40. 68 0
      docfx/docs/migratingfromv1.md
  41. 50 6
      docfx/docs/multitasking.md
  42. 10 5
      docfx/docs/navigation.md
  43. 41 0
      docfx/docs/newinv2.md
  44. 12 8
      docfx/docs/toc.yml

+ 5 - 0
.config/dotnet-tools.json

@@ -0,0 +1,5 @@
+{
+  "version": 1,
+  "isRoot": true,
+  "tools": {}
+}

+ 1 - 1
Examples/UICatalog/Scenarios/AllViewsTester.cs

@@ -65,7 +65,7 @@ public class AllViewsTester : Scenario
                                                   // Dispose existing current View, if any
                                                   DisposeCurrentView ();
 
-                                                  CreateCurrentView (_viewClasses.Values.ToArray () [_classListView.SelectedItem]);
+                                                  CreateCurrentView (_viewClasses.Values.ToArray () [_classListView.SelectedItem.Value]);
 
                                                   // Force ViewToEdit to be the view and not a subview
                                                   if (_adornmentsEditor is { })

+ 2 - 2
Examples/UICatalog/Scenarios/ComboBoxIteration.cs

@@ -42,8 +42,8 @@ public class ComboBoxIteration : Scenario
 
         listview.SelectedItemChanged += (s, e) =>
                                         {
-                                            lbListView.Text = items [e.Item];
-                                            comboBox.SelectedItem = e.Item;
+                                            lbListView.Text = items [e.Item!.Value];
+                                            comboBox.SelectedItem = e.Item.Value;
                                         };
 
         comboBox.SelectedItemChanged += (sender, text) =>

+ 12 - 12
Examples/UICatalog/Scenarios/DynamicMenuBar.cs

@@ -712,7 +712,7 @@ public class DynamicMenuBar : Scenario
 
             btnUp.Accepting += (s, e) =>
                              {
-                                 int i = _lstMenus.SelectedItem;
+                                 int i = _lstMenus.SelectedItem.Value;
                                  MenuItem menuItem = DataContext.Menus.Count > 0 ? DataContext.Menus [i].MenuItem : null;
 
                                  if (menuItem != null)
@@ -734,7 +734,7 @@ public class DynamicMenuBar : Scenario
 
             btnDown.Accepting += (s, e) =>
                                {
-                                   int i = _lstMenus.SelectedItem;
+                                   int i = _lstMenus.SelectedItem.Value;
                                    MenuItem menuItem = DataContext.Menus.Count > 0 ? DataContext.Menus [i].MenuItem : null;
 
                                    if (menuItem != null)
@@ -836,7 +836,7 @@ public class DynamicMenuBar : Scenario
                                                               : MenuItemCheckStyle.Radio,
                                          ShortcutKey = frmMenuDetails.TextShortcutKey.Text
                                      };
-                                     UpdateMenuItem (_currentEditMenuBarItem, menuItem, _lstMenus.SelectedItem);
+                                     UpdateMenuItem (_currentEditMenuBarItem, menuItem, _lstMenus.SelectedItem.Value);
                                  }
                              };
 
@@ -885,8 +885,8 @@ public class DynamicMenuBar : Scenario
 
             btnRemove.Accepting += (s, e) =>
                                 {
-                                    MenuItem menuItem = (DataContext.Menus.Count > 0 && _lstMenus.SelectedItem > -1
-                                                             ? DataContext.Menus [_lstMenus.SelectedItem].MenuItem
+                                    MenuItem menuItem = (DataContext.Menus.Count > 0 && _lstMenus.SelectedItem is {} selectedItem
+                                                             ? DataContext.Menus [selectedItem].MenuItem
                                                              : _currentEditMenuBarItem);
 
                                     if (menuItem != null)
@@ -905,9 +905,9 @@ public class DynamicMenuBar : Scenario
                                             SelectCurrentMenuBarItem ();
                                         }
 
-                                        if (_lstMenus.SelectedItem > -1)
+                                        if (_lstMenus.SelectedItem is {} selected)
                                         {
-                                            DataContext.Menus?.RemoveAt (_lstMenus.SelectedItem);
+                                            DataContext.Menus?.RemoveAt (selected);
                                         }
 
                                         if (_lstMenus.Source.Count > 0 && _lstMenus.SelectedItem > _lstMenus.Source.Count - 1)
@@ -927,7 +927,7 @@ public class DynamicMenuBar : Scenario
 
             _lstMenus.OpenSelectedItem += (s, e) =>
                                           {
-                                              _currentMenuBarItem = DataContext.Menus [e.Item].MenuItem;
+                                              _currentMenuBarItem = DataContext.Menus [e.Item.Value].MenuItem;
 
                                               if (!(_currentMenuBarItem is MenuBarItem))
                                               {
@@ -945,8 +945,8 @@ public class DynamicMenuBar : Scenario
 
             _lstMenus.HasFocusChanging += (s, e) =>
                                {
-                                   MenuItem menuBarItem = _lstMenus.SelectedItem > -1 && DataContext.Menus.Count > 0
-                                                              ? DataContext.Menus [_lstMenus.SelectedItem].MenuItem
+                                   MenuItem menuBarItem = _lstMenus.SelectedItem is {} selectedItem && DataContext.Menus.Count > 0
+                                                              ? DataContext.Menus [selectedItem].MenuItem
                                                               : null;
                                    SetFrameDetails (menuBarItem);
                                };
@@ -1077,8 +1077,8 @@ public class DynamicMenuBar : Scenario
 
                 if (menuBarItem == null)
                 {
-                    menuItem = _lstMenus.SelectedItem > -1 && DataContext.Menus.Count > 0
-                                   ? DataContext.Menus [_lstMenus.SelectedItem].MenuItem
+                    menuItem = _lstMenus.SelectedItem is {} selectedItem && DataContext.Menus.Count > 0
+                                   ? DataContext.Menus [selectedItem].MenuItem
                                    : _currentEditMenuBarItem;
                 }
                 else

+ 21 - 8
Examples/UICatalog/Scenarios/DynamicStatusBar.cs

@@ -312,7 +312,12 @@ public class DynamicStatusBar : Scenario
 
             btnUp.Accepting += (s, e) =>
                               {
-                                  int i = _lstItems.SelectedItem;
+                                  if (_lstItems.SelectedItem is null)
+                                  {
+                                      return;
+                                  }
+                                  int i = _lstItems.SelectedItem.Value;
+
                                   Shortcut statusItem = DataContext.Items.Count > 0 ? DataContext.Items [i].Shortcut : null;
 
                                   if (statusItem != null)
@@ -335,7 +340,12 @@ public class DynamicStatusBar : Scenario
 
             btnDown.Accepting += (s, e) =>
                                 {
-                                    int i = _lstItems.SelectedItem;
+                                    if (_lstItems.SelectedItem is null)
+                                    {
+                                        return;
+                                    }
+                                    int i = _lstItems.SelectedItem.Value;
+
                                     Shortcut statusItem = DataContext.Items.Count > 0 ? DataContext.Items [i].Shortcut : null;
 
                                     if (statusItem != null)
@@ -376,14 +386,17 @@ public class DynamicStatusBar : Scenario
                                   }
                                   else if (_currentEditStatusItem != null)
                                   {
-
                                       var statusItem = new DynamicStatusItem
                                       {
                                           Title = frmStatusBarDetails.TextTitle.Text,
                                           Action = frmStatusBarDetails.TextAction.Text,
                                           Shortcut = frmStatusBarDetails.TextShortcut.Text
                                       };
-                                      UpdateStatusItem (_currentEditStatusItem, statusItem, _lstItems.SelectedItem);
+
+                                      if (_lstItems.SelectedItem is { } selectedItem)
+                                      {
+                                          UpdateStatusItem (_currentEditStatusItem, statusItem, selectedItem);
+                                      }
                                   }
                               };
 
@@ -420,14 +433,14 @@ public class DynamicStatusBar : Scenario
             btnRemove.Accepting += (s, e) =>
                                   {
                                       Shortcut statusItem = DataContext.Items.Count > 0
-                                                                  ? DataContext.Items [_lstItems.SelectedItem].Shortcut
+                                                                  ? DataContext.Items [_lstItems.SelectedItem.Value].Shortcut
                                                                   : null;
 
                                       if (statusItem != null)
                                       {
                                           _statusBar.RemoveShortcut (_currentSelectedStatusBar);
                                           statusItem.Dispose ();
-                                          DataContext.Items.RemoveAt (_lstItems.SelectedItem);
+                                          DataContext.Items.RemoveAt (_lstItems.SelectedItem.Value);
 
                                           if (_lstItems.Source.Count > 0 && _lstItems.SelectedItem > _lstItems.Source.Count - 1)
                                           {
@@ -442,7 +455,7 @@ public class DynamicStatusBar : Scenario
             _lstItems.HasFocusChanging += (s, e) =>
                                {
                                    Shortcut statusItem = DataContext.Items.Count > 0
-                                                               ? DataContext.Items [_lstItems.SelectedItem].Shortcut
+                                                               ? DataContext.Items [_lstItems.SelectedItem.Value].Shortcut
                                                                : null;
                                    SetFrameDetails (statusItem);
                                };
@@ -489,7 +502,7 @@ public class DynamicStatusBar : Scenario
                 if (statusItem == null)
                 {
                     newStatusItem = DataContext.Items.Count > 0
-                                        ? DataContext.Items [_lstItems.SelectedItem].Shortcut
+                                        ? DataContext.Items [_lstItems.SelectedItem.Value].Shortcut
                                         : null;
                 }
                 else

+ 4 - 4
Examples/UICatalog/Scenarios/ListViewWithSelection.cs

@@ -237,7 +237,7 @@ public class ListViewWithSelection : Scenario
             int col,
             int line,
             int width,
-            int start = 0
+            int viewportX = 0
         )
         {
             container.Move (col, line);
@@ -247,7 +247,7 @@ public class ListViewWithSelection : Scenario
                                       string.Format ("{{0,{0}}}", -_nameColumnWidth),
                                       Scenarios [item].GetName ()
                                      );
-            RenderUstr (container, $"{s} ({Scenarios [item].GetDescription ()})", col, line, width, start);
+            RenderUstr (container, $"{s} ({Scenarios [item].GetDescription ()})", col, line, width, viewportX);
         }
 
         public void SetMark (int item, bool value)
@@ -288,10 +288,10 @@ public class ListViewWithSelection : Scenario
         }
 
         // A slightly adapted method from: https://github.com/gui-cs/Terminal.Gui/blob/fc1faba7452ccbdf49028ac49f0c9f0f42bbae91/Terminal.Gui/Views/ListView.cs#L433-L461
-        private void RenderUstr (View view, string ustr, int col, int line, int width, int start = 0)
+        private void RenderUstr (View view, string ustr, int col, int line, int width, int viewportX = 0)
         {
             var used = 0;
-            int index = start;
+            int index = viewportX;
 
             while (index < ustr.Length)
             {

+ 1 - 1
Examples/UICatalog/Scenarios/ListsAndCombos.cs

@@ -50,7 +50,7 @@ public class ListsAndCombos : Scenario
             Width = Dim.Percent (40),
             Source = new ListWrapper<string> (items)
         };
-        listview.SelectedItemChanged += (s, e) => lbListView.Text = items [listview.SelectedItem];
+        listview.SelectedItemChanged += (s, e) => lbListView.Text = items [listview.SelectedItem.Value];
         win.Add (lbListView, listview);
 
         //var scrollBar = new ScrollBarView (listview, true);

+ 1 - 1
Examples/UICatalog/Scenarios/SpinnerStyles.cs

@@ -153,7 +153,7 @@ public class SpinnerViewStyles : Scenario
                                           else
                                           {
                                               spinner.Visible = true;
-                                              spinner.Style = (SpinnerStyle)Activator.CreateInstance (styleDict [e.Item].Value);
+                                              spinner.Style = (SpinnerStyle)Activator.CreateInstance (styleDict [e.Item.Value].Value);
                                               delayField.Text = spinner.SpinDelay.ToString ();
                                               ckbBounce.CheckedState = spinner.SpinBounce ? CheckState.Checked : CheckState.UnChecked;
                                               ckbNoSpecial.CheckedState = !spinner.HasSpecialCharacters ? CheckState.Checked : CheckState.UnChecked;

+ 11 - 3
Examples/UICatalog/UICatalogTop.cs

@@ -43,7 +43,11 @@ public class UICatalogTop : Toplevel
         Unloaded += UnloadedHandler;
 
         // Restore previous selections
-        _categoryList.SelectedItem = _cachedCategoryIndex;
+        if (_categoryList.Source?.Count > 0) {
+            _categoryList.SelectedItem = _cachedCategoryIndex ?? 0;
+        } else {
+            _categoryList.SelectedItem = null;
+        }
         _scenarioList.SelectedRow = _cachedScenarioIndex;
 
         SchemeName = CachedTopLevelScheme = SchemeManager.SchemesToSchemeName (Schemes.Base);
@@ -510,7 +514,7 @@ public class UICatalogTop : Toplevel
     #region Category List
 
     private readonly ListView? _categoryList;
-    private static int _cachedCategoryIndex;
+    private static int? _cachedCategoryIndex;
     public static ObservableCollection<string>? CachedCategories { get; set; }
 
     private ListView CreateCategoryList ()
@@ -540,7 +544,11 @@ public class UICatalogTop : Toplevel
 
     private void CategoryView_SelectedChanged (object? sender, ListViewItemEventArgs? e)
     {
-        string item = CachedCategories! [e!.Item];
+        if (e is null or { Item: null })
+        {
+            return;
+        }
+        string item = CachedCategories! [e.Item.Value];
         ObservableCollection<Scenario> newScenarioList;
 
         if (e.Item == 0)

+ 1 - 1
Scripts/Run-LocalCoverage.ps1

@@ -27,7 +27,7 @@ dotnet test Tests/UnitTests `
   --verbosity minimal `
   --collect:"XPlat Code Coverage" `
   --settings Tests/UnitTests/runsettings.coverage.xml `
-  --blame-hang-timeout 10s
+  --blame-hang-timeout 60s
 
 # ------------------------------------------------------------
 # 4. Run UNIT TESTS (parallel)

+ 40 - 34
Terminal.Gui/Views/CollectionNavigation/CollectionNavigatorBase.cs

@@ -1,6 +1,5 @@
 
 
-
 namespace Terminal.Gui.Views;
 
 /// <inheritdoc/>
@@ -27,8 +26,13 @@ internal abstract class CollectionNavigatorBase : ICollectionNavigator
     public int TypingDelay { get; set; } = 500;
 
     /// <inheritdoc/>
-    public int GetNextMatchingItem (int currentIndex, char keyStruck)
+    public int? GetNextMatchingItem (int? currentIndex, char keyStruck)
     {
+        if (currentIndex.HasValue && currentIndex < 0)
+        {
+            throw new ArgumentOutOfRangeException (nameof (currentIndex), @"Must be non-negative");
+        }
+
         if (!char.IsControl (keyStruck))
         {
             // maybe user pressed 'd' and now presses 'd' again.
@@ -36,7 +40,7 @@ internal abstract class CollectionNavigatorBase : ICollectionNavigator
             // but if we find none then we must fallback on cycling
             // d instead and discard the candidate state
             var candidateState = "";
-            var elapsedTime = DateTime.Now - _lastKeystroke;
+            TimeSpan elapsedTime = DateTime.Now - _lastKeystroke;
 
             Logging.Debug ($"CollectionNavigator began processing '{keyStruck}', it has been {elapsedTime} since last keystroke");
 
@@ -51,26 +55,28 @@ internal abstract class CollectionNavigatorBase : ICollectionNavigator
             {
                 // its a fresh keystroke after some time
                 // or its first ever key press
-                SearchString = new string (keyStruck, 1);
-                Logging.Debug ($"It has been too long since last key press so beginning new search");
+                SearchString = new (keyStruck, 1);
+                Logging.Debug ("It has been too long since last key press so beginning new search");
             }
 
-            int idxCandidate = GetNextMatchingItem (
-                                                    currentIndex,
-                                                    candidateState,
+            int? idxCandidate = GetNextMatchingItem (
+                                                     currentIndex,
+                                                     candidateState,
 
-                                                    // prefer not to move if there are multiple characters e.g. "ca" + 'r' should stay on "car" and not jump to "cart"
-                                                    candidateState.Length > 1
-                                                   );
+                                                     // prefer not to move if there are multiple characters e.g. "ca" + 'r' should stay on "car" and not jump to "cart"
+                                                     candidateState.Length > 1
+                                                    );
 
             Logging.Debug ($"CollectionNavigator searching (preferring minimum movement) matched:{idxCandidate}");
-            if (idxCandidate != -1)
+
+            if (idxCandidate is { })
             {
                 // found "dd" so candidate search string is accepted
                 _lastKeystroke = DateTime.Now;
                 SearchString = candidateState;
 
                 Logging.Debug ($"Found collection item that matched search:{idxCandidate}");
+
                 return idxCandidate;
             }
 
@@ -83,16 +89,17 @@ internal abstract class CollectionNavigatorBase : ICollectionNavigator
 
             // if a match wasn't found, the user typed a 'wrong' key in their search ("can" + 'z'
             // instead of "can" + 'd').
-            if (SearchString.Length > 1 && idxCandidate == -1)
+            if (SearchString.Length > 1 && idxCandidate is null)
             {
                 Logging.Debug ("CollectionNavigator ignored key and returned existing index");
+
                 // ignore it since we're still within the typing delay
                 // don't add it to SearchString either
                 return currentIndex;
             }
 
             // if no changes to current state manifested
-            if (idxCandidate == currentIndex || idxCandidate == -1)
+            if (idxCandidate == currentIndex || idxCandidate is null)
             {
                 Logging.Debug ("CollectionNavigator found no changes to current index, so clearing search");
 
@@ -100,37 +107,29 @@ internal abstract class CollectionNavigatorBase : ICollectionNavigator
                 ClearSearchString ();
 
                 // match on the fresh letter alone
-                SearchString = new string (keyStruck, 1);
+                SearchString = new (keyStruck, 1);
                 idxCandidate = GetNextMatchingItem (currentIndex, SearchString);
 
                 Logging.Debug ($"CollectionNavigator new SearchString {SearchString} matched index:{idxCandidate}");
 
-                return idxCandidate == -1 ? currentIndex : idxCandidate;
+                return idxCandidate ?? currentIndex;
             }
 
             Logging.Debug ($"CollectionNavigator final answer was:{idxCandidate}");
+
             // Found another "d" or just leave index as it was
             return idxCandidate;
         }
 
-        Logging.Debug ("CollectionNavigator found key press was not actionable so clearing search and returning -1");
+        Logging.Debug ("CollectionNavigator found key press was not actionable so clearing search and returning null");
 
         // clear state because keypress was a control char
         ClearSearchString ();
 
         // control char indicates no selection
-        return -1;
+        return null;
     }
 
-
-
-    /// <summary>
-    ///     Raised when the <see cref="SearchString"/> is changed. Useful for debugging. Raises the
-    ///     <see cref="SearchStringChanged"/> event.
-    /// </summary>
-    /// <param name="e"></param>
-    protected virtual void OnSearchStringChanged (KeystrokeNavigatorEventArgs e) { SearchStringChanged?.Invoke (this, e); }
-
     /// <summary>This event is raised when <see cref="SearchString"/> is changed. Useful for debugging.</summary>
     public event EventHandler<KeystrokeNavigatorEventArgs>? SearchStringChanged;
 
@@ -141,6 +140,13 @@ internal abstract class CollectionNavigatorBase : ICollectionNavigator
     /// <summary>Return the number of elements in the collection</summary>
     protected abstract int GetCollectionLength ();
 
+    /// <summary>
+    ///     Raised when the <see cref="SearchString"/> is changed. Useful for debugging. Raises the
+    ///     <see cref="SearchStringChanged"/> event.
+    /// </summary>
+    /// <param name="e"></param>
+    protected virtual void OnSearchStringChanged (KeystrokeNavigatorEventArgs e) { SearchStringChanged?.Invoke (this, e); }
+
     /// <summary>Gets the index of the next item in the collection that matches <paramref name="search"/>.</summary>
     /// <param name="currentIndex">The index in the collection to start the search from.</param>
     /// <param name="search">The search string to use.</param>
@@ -150,17 +156,17 @@ internal abstract class CollectionNavigatorBase : ICollectionNavigator
     ///     <see langword="false"/> (the default), the next matching item will be returned, even if it is above in the
     ///     collection.
     /// </param>
-    /// <returns>The index of the next matching item or <see langword="-1"/> if no match was found.</returns>
-    internal int GetNextMatchingItem (int currentIndex, string search, bool minimizeMovement = false)
+    /// <returns>The index of the next matching item or <see langword="null"/> if no match was found.</returns>
+    internal int? GetNextMatchingItem (int? currentIndex, string search, bool minimizeMovement = false)
     {
         if (string.IsNullOrEmpty (search))
         {
-            return -1;
+            return null;
         }
 
         int collectionLength = GetCollectionLength ();
 
-        if (currentIndex != -1 && currentIndex < collectionLength && Matcher.IsMatch (search, ElementAt (currentIndex)))
+        if (currentIndex.HasValue && currentIndex < collectionLength && Matcher.IsMatch (search, ElementAt (currentIndex.Value)))
         {
             // we are already at a match
             if (minimizeMovement)
@@ -172,9 +178,9 @@ internal abstract class CollectionNavigatorBase : ICollectionNavigator
             for (var i = 1; i < collectionLength; i++)
             {
                 //circular
-                int idxCandidate = (i + currentIndex) % collectionLength;
+                int? idxCandidate = (i + currentIndex) % collectionLength;
 
-                if (Matcher.IsMatch (search, ElementAt (idxCandidate)))
+                if (Matcher.IsMatch (search, ElementAt (idxCandidate!.Value)))
                 {
                     return idxCandidate;
                 }
@@ -194,7 +200,7 @@ internal abstract class CollectionNavigatorBase : ICollectionNavigator
         }
 
         // Nothing matches
-        return -1;
+        return null;
     }
 
     private void ClearSearchString ()

+ 4 - 4
Terminal.Gui/Views/CollectionNavigation/ICollectionNavigator.cs

@@ -6,7 +6,7 @@ namespace Terminal.Gui.Views;
 /// <summary>
 ///     Navigates a collection of items using keystrokes. The keystrokes are used to build a search string. The
 ///     <see cref="SearchString"/> is used to find the next item in the collection that matches the search string when
-///     <see cref="GetNextMatchingItem(int, char)"/> is called.
+///     <see cref="GetNextMatchingItem(int?, char)"/> is called.
 ///     <para>
 ///         If the user types keystrokes that can't be found in the collection, the search string is cleared and the next
 ///         item is found that starts with the last keystroke.
@@ -17,7 +17,7 @@ public interface ICollectionNavigator
 {
     /// <summary>
     ///     Gets or sets the number of milliseconds to delay before clearing the search string. The delay is reset on each
-    ///     call to <see cref="GetNextMatchingItem(int, char)"/>. The default is 500ms.
+    ///     call to <see cref="GetNextMatchingItem(int?, char)"/>. The default is 500ms.
     /// </summary>
     public int TypingDelay { get; set; }
 
@@ -43,8 +43,8 @@ public interface ICollectionNavigator
     /// <param name="currentIndex">The index in the collection to start the search from.</param>
     /// <param name="keyStruck">The character of the key the user pressed.</param>
     /// <returns>
-    ///     The index of the item that matches what the user has typed. Returns <see langword="-1"/> if no item in the
+    ///     The index of the item that matches what the user has typed. Returns <see langword="null"/> if no item in the
     ///     collection matched.
     /// </returns>
-    int GetNextMatchingItem (int currentIndex, char keyStruck);
+    int? GetNextMatchingItem (int? currentIndex, char keyStruck);
 }

+ 17 - 10
Terminal.Gui/Views/ComboBox.cs

@@ -47,9 +47,9 @@ public class ComboBox : View, IDesignable
                               };
         _listview.SelectedItemChanged += (sender, e) =>
                                          {
-                                             if (!HideDropdownListOnClick && _searchSet.Count > 0)
+                                             if (e.Item >= 0 && !HideDropdownListOnClick && _searchSet.Count > 0)
                                              {
-                                                 SetValue (_searchSet [_listview.SelectedItem]);
+                                                 SetValue (_searchSet [e.Item.Value]);
                                              }
                                          };
         Add (_search, _listview);
@@ -114,7 +114,7 @@ public class ComboBox : View, IDesignable
     /// <inheritdoc />
     protected override bool OnSettingScheme (ValueChangingEventArgs<Scheme> args)
     {
-        _listview.SetScheme(args.NewValue);
+        _listview.SetScheme (args.NewValue);
         return base.OnSettingScheme (args);
     }
 
@@ -461,7 +461,10 @@ public class ComboBox : View, IDesignable
 
     private void FocusSelectedItem ()
     {
-        _listview.SelectedItem = SelectedItem > -1 ? SelectedItem : 0;
+        if (_listview.Source?.Count > 0)
+        {
+            _listview.SelectedItem = SelectedItem > -1 ? SelectedItem : 0;
+        }
         _listview.TabStop = TabBehavior.TabStop;
         _listview.SetFocus ();
         OnExpanded ();
@@ -517,9 +520,9 @@ public class ComboBox : View, IDesignable
                 _listview.TabStop = TabBehavior.TabStop;
                 _listview.SetFocus ();
 
-                if (_listview.SelectedItem > -1)
+                if (_listview.SelectedItem is { })
                 {
-                    SetValue (_searchSet [_listview.SelectedItem]);
+                    SetValue (_searchSet [_listview.SelectedItem.Value]);
                 }
                 else
                 {
@@ -728,7 +731,7 @@ public class ComboBox : View, IDesignable
         IsShow = false;
         _listview.TabStop = TabBehavior.NoStop;
 
-        if (_listview.Source.Count == 0 || (_searchSet?.Count ?? 0) == 0)
+        if (_listview.Source!.Count == 0 || (_searchSet?.Count ?? 0) == 0)
         {
             _text = "";
             HideList ();
@@ -737,7 +740,7 @@ public class ComboBox : View, IDesignable
             return false;
         }
 
-        SetValue (_listview.SelectedItem > -1 ? _searchSet [_listview.SelectedItem] : _text);
+        SetValue (_listview.SelectedItem is { } ? _searchSet [_listview.SelectedItem.Value] : _text);
         _search.CursorPosition = _search.Text.GetColumns ();
         ShowHideList (Text);
         OnOpenSelectedItem ();
@@ -977,7 +980,11 @@ public class ComboBox : View, IDesignable
         {
             bool res = base.OnSelectedChanged ();
 
-            _highlighted = SelectedItem;
+            if (SelectedItem is null)
+            {
+                return res;
+            }
+            _highlighted = SelectedItem.Value;
 
             return res;
         }
@@ -997,7 +1004,7 @@ public class ComboBox : View, IDesignable
             _container = container
                          ?? throw new ArgumentNullException (
                                                              nameof (container),
-                                                             "ComboBox container cannot be null."
+                                                             @"ComboBox container cannot be null."
                                                             );
             HideDropdownListOnClick = hideDropdownListOnClick;
             AddCommand (Command.Up, () => _container.MoveUpList ());

+ 75 - 32
Terminal.Gui/Views/IListDataSource.cs

@@ -4,43 +4,68 @@ using System.Collections.Specialized;
 
 namespace Terminal.Gui.Views;
 
-/// <summary>Implement <see cref="IListDataSource"/> to provide custom rendering for a <see cref="ListView"/>.</summary>
+/// <summary>
+///     Provides data and rendering for <see cref="ListView"/>. Implement this interface to provide custom rendering
+///     or to wrap custom data sources.
+/// </summary>
+/// <remarks>
+///     <para>
+///         The default implementation is <see cref="ListWrapper{T}"/> which renders items using
+///         <see cref="object.ToString()"/>.
+///     </para>
+///     <para>
+///         Implementors must manage their own marking state and raise <see cref="CollectionChanged"/> when the
+///         underlying data changes.
+///     </para>
+/// </remarks>
 public interface IListDataSource : IDisposable
 {
     /// <summary>
-    /// Event to raise when an item is added, removed, or moved, or the entire list is refreshed.
+    ///     Raised when items are added, removed, moved, or the entire collection is refreshed.
     /// </summary>
+    /// <remarks>
+    ///     <see cref="ListView"/> subscribes to this event to update its display and content size when the data
+    ///     changes. Implementations should raise this event whenever the underlying collection changes, unless
+    ///     <see cref="SuspendCollectionChangedEvent"/> is <see langword="true"/>.
+    /// </remarks>
     event NotifyCollectionChangedEventHandler CollectionChanged;
 
-    /// <summary>Returns the number of elements to display</summary>
+    /// <summary>Gets the number of items in the data source.</summary>
     int Count { get; }
 
-    /// <summary>Returns the maximum length of elements to display</summary>
-    int Length { get; }
-
-    /// <summary>
-    /// Allow suspending the <see cref="CollectionChanged"/> event from being invoked,
-    /// if <see langword="true"/>, otherwise is <see langword="false"/>.
-    /// </summary>
-    bool SuspendCollectionChangedEvent { get; set; }
-
-    /// <summary>Should return whether the specified item is currently marked.</summary>
-    /// <returns><see langword="true"/>, if marked, <see langword="false"/> otherwise.</returns>
-    /// <param name="item">Item index.</param>
+    /// <summary>Determines whether the specified item is marked.</summary>
+    /// <param name="item">The zero-based index of the item.</param>
+    /// <returns><see langword="true"/> if the item is marked; otherwise <see langword="false"/>.</returns>
+    /// <remarks>
+    ///     <see cref="ListView"/> calls this method to determine whether to render the item with a mark indicator when
+    ///     <see cref="ListView.AllowsMarking"/> is <see langword="true"/>.
+    /// </remarks>
     bool IsMarked (int item);
 
-    /// <summary>This method is invoked to render a specified item, the method should cover the entire provided width.</summary>
-    /// <returns>The render.</returns>
-    /// <param name="listView">The list view to render.</param>
-    /// <param name="selected">Describes whether the item being rendered is currently selected by the user.</param>
-    /// <param name="item">The index of the item to render, zero for the first item and so on.</param>
-    /// <param name="col">The column where the rendering will start</param>
-    /// <param name="line">The line where the rendering will be done.</param>
-    /// <param name="width">The width that must be filled out.</param>
-    /// <param name="start">The index of the string to be displayed.</param>
+    /// <summary>Gets the width in columns of the widest item in the data source.</summary>
+    /// <remarks>
+    ///     <see cref="ListView"/> uses this value to set its horizontal content size for scrolling.
+    /// </remarks>
+    int Length { get; }
+
+    /// <summary>Renders the specified item to the <see cref="ListView"/>.</summary>
+    /// <param name="listView">The <see cref="ListView"/> to render to.</param>
+    /// <param name="selected">
+    ///     <see langword="true"/> if the item is currently selected; otherwise <see langword="false"/>.
+    /// </param>
+    /// <param name="item">The zero-based index of the item to render.</param>
+    /// <param name="col">The column in <paramref name="listView"/> where rendering starts.</param>
+    /// <param name="line">The line in <paramref name="listView"/> where rendering occurs.</param>
+    /// <param name="width">The width available for rendering.</param>
+    /// <param name="viewportX">The horizontal scroll offset.</param>
     /// <remarks>
-    ///     The default color will be set before this method is invoked, and will be based on whether the item is selected
-    ///     or not.
+    ///     <para>
+    ///         <see cref="ListView"/> calls this method for each visible item during rendering. The color scheme will be
+    ///         set based on selection state before this method is called.
+    ///     </para>
+    ///     <para>
+    ///         Implementations must fill the entire <paramref name="width"/> to avoid rendering artifacts.
+    ///     </para>
     /// </remarks>
     void Render (
         ListView listView,
@@ -49,15 +74,33 @@ public interface IListDataSource : IDisposable
         int col,
         int line,
         int width,
-        int start = 0
+        int viewportX = 0
     );
 
-    /// <summary>Flags the item as marked.</summary>
-    /// <param name="item">Item index.</param>
-    /// <param name="value">If set to <see langword="true"/> value.</param>
+    /// <summary>Sets the marked state of the specified item.</summary>
+    /// <param name="item">The zero-based index of the item.</param>
+    /// <param name="value"><see langword="true"/> to mark the item; <see langword="false"/> to unmark it.</param>
+    /// <remarks>
+    ///     <see cref="ListView"/> calls this method when the user toggles marking (e.g., via the SPACE key) if
+    ///     <see cref="ListView.AllowsMarking"/> is <see langword="true"/>.
+    /// </remarks>
     void SetMark (int item, bool value);
 
-    /// <summary>Return the source as IList.</summary>
-    /// <returns></returns>
+    /// <summary>
+    ///     Gets or sets whether the <see cref="CollectionChanged"/> event should be suppressed.
+    /// </summary>
+    /// <remarks>
+    ///     Set to <see langword="true"/> to prevent <see cref="CollectionChanged"/> from being raised during bulk
+    ///     operations. Set back to <see langword="false"/> to resume event notifications.
+    /// </remarks>
+    bool SuspendCollectionChangedEvent { get; set; }
+
+    /// <summary>Returns the underlying data source as an <see cref="IList"/>.</summary>
+    /// <returns>The data source as an <see cref="IList"/>.</returns>
+    /// <remarks>
+    ///     <see cref="ListView"/> uses this method to access individual items for events like
+    ///     <see cref="ListView.SelectedItemChanged"/> and to enable keyboard search via
+    ///     <see cref="ListView.KeystrokeNavigator"/>.
+    /// </remarks>
     IList ToList ();
 }

Diferenças do arquivo suprimidas por serem muito extensas
+ 234 - 517
Terminal.Gui/Views/ListView.cs


+ 4 - 5
Terminal.Gui/Views/ListViewEventArgs.cs

@@ -1,5 +1,4 @@
-#nullable disable
-namespace Terminal.Gui.Views;
+namespace Terminal.Gui.Views;
 
 /// <summary><see cref="EventArgs"/> for <see cref="ListView"/> events.</summary>
 public class ListViewItemEventArgs : EventArgs
@@ -7,17 +6,17 @@ public class ListViewItemEventArgs : EventArgs
     /// <summary>Initializes a new instance of <see cref="ListViewItemEventArgs"/></summary>
     /// <param name="item">The index of the <see cref="ListView"/> item.</param>
     /// <param name="value">The <see cref="ListView"/> item</param>
-    public ListViewItemEventArgs (int item, object value)
+    public ListViewItemEventArgs (int? item, object? value)
     {
         Item = item;
         Value = value;
     }
 
     /// <summary>The index of the <see cref="ListView"/> item.</summary>
-    public int Item { get; }
+    public int? Item { get; }
 
     /// <summary>The <see cref="ListView"/> item.</summary>
-    public object Value { get; }
+    public object? Value { get; }
 }
 
 /// <summary><see cref="EventArgs"/> used by the <see cref="ListView.RowRender"/> event.</summary>

+ 256 - 0
Terminal.Gui/Views/ListWrapper.cs

@@ -0,0 +1,256 @@
+#nullable enable
+using System.Collections;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+
+namespace Terminal.Gui.Views;
+
+/// <summary>
+///     Provides a default implementation of <see cref="IListDataSource"/> that renders <see cref="ListView"/> items
+///     using <see cref="object.ToString()"/>.
+/// </summary>
+public class ListWrapper<T> : IListDataSource, IDisposable
+{
+    /// <summary>
+    ///     Creates a new instance of <see cref="ListWrapper{T}"/> that wraps the specified
+    ///     <see cref="ObservableCollection{T}"/>.
+    /// </summary>
+    /// <param name="source"></param>
+    public ListWrapper (ObservableCollection<T>? source)
+    {
+        if (source is { })
+        {
+            _count = source.Count;
+            _marks = new (_count);
+            _source = source;
+            _source.CollectionChanged += Source_CollectionChanged;
+            Length = GetMaxLengthItem ();
+        }
+    }
+
+    private readonly ObservableCollection<T>? _source;
+    private int _count;
+    private BitArray? _marks;
+
+    private bool _suspendCollectionChangedEvent;
+
+    /// <inheritdoc/>
+    public event NotifyCollectionChangedEventHandler? CollectionChanged;
+
+    /// <inheritdoc/>
+    public int Count => _source?.Count ?? 0;
+
+    /// <inheritdoc/>
+    public int Length { get; private set; }
+
+    /// <inheritdoc/>
+    public bool SuspendCollectionChangedEvent
+    {
+        get => _suspendCollectionChangedEvent;
+        set
+        {
+            _suspendCollectionChangedEvent = value;
+
+            if (!_suspendCollectionChangedEvent)
+            {
+                CheckAndResizeMarksIfRequired ();
+            }
+        }
+    }
+
+    /// <inheritdoc/>
+    public void Render (
+        ListView container,
+        bool marked,
+        int item,
+        int col,
+        int line,
+        int width,
+        int viewportX = 0
+    )
+    {
+        container.Move (Math.Max (col - viewportX, 0), line);
+
+        if (_source is null)
+        {
+            return;
+        }
+
+        object? t = _source [item];
+
+        if (t is null)
+        {
+            RenderString (container, "", col, line, width);
+        }
+        else
+        {
+            if (t is string s)
+            {
+                RenderString (container, s, col, line, width, viewportX);
+            }
+            else
+            {
+                RenderString (container, t.ToString ()!, col, line, width, viewportX);
+            }
+        }
+    }
+
+    /// <inheritdoc/>
+    public bool IsMarked (int item)
+    {
+        if (item >= 0 && item < _count)
+        {
+            return _marks! [item];
+        }
+
+        return false;
+    }
+
+    /// <inheritdoc/>
+    public void SetMark (int item, bool value)
+    {
+        if (item >= 0 && item < _count)
+        {
+            _marks! [item] = value;
+        }
+    }
+
+    /// <inheritdoc/>
+    public IList ToList () { return _source ?? []; }
+
+    /// <inheritdoc/>
+    public void Dispose ()
+    {
+        if (_source is { })
+        {
+            _source.CollectionChanged -= Source_CollectionChanged;
+        }
+    }
+
+    /// <summary>
+    ///     INTERNAL: Searches the underlying collection for the first string element that starts with the specified search value,
+    ///     using a case-insensitive comparison.
+    /// </summary>
+    /// <remarks>
+    ///     The comparison is performed in a case-insensitive manner using invariant culture rules. Only
+    ///     elements of type string are considered; other types in the collection are ignored.
+    /// </remarks>
+    /// <param name="search">
+    ///     The string value to compare against the start of each string element in the collection. Cannot be
+    ///     null.
+    /// </param>
+    /// <returns>
+    ///     The zero-based index of the first matching string element if found; otherwise, -1 if no match is found or the
+    ///     collection is empty.
+    /// </returns>
+    internal int StartsWith (string search)
+    {
+        if (_source is null || _source?.Count == 0)
+        {
+            return -1;
+        }
+
+        for (var i = 0; i < _source!.Count; i++)
+        {
+            object? t = _source [i];
+
+            if (t is string u)
+            {
+                if (u.ToUpper ().StartsWith (search.ToUpperInvariant ()))
+                {
+                    return i;
+                }
+            }
+            else if (t is string s && s.StartsWith (search, StringComparison.InvariantCultureIgnoreCase))
+            {
+                return i;
+            }
+        }
+
+        return -1;
+    }
+
+    private void CheckAndResizeMarksIfRequired ()
+    {
+        if (_source != null && _count != _source.Count && _marks is { })
+        {
+            _count = _source.Count;
+            var newMarks = new BitArray (_count);
+
+            for (var i = 0; i < Math.Min (_marks.Length, newMarks.Length); i++)
+            {
+                newMarks [i] = _marks [i];
+            }
+
+            _marks = newMarks;
+
+            Length = GetMaxLengthItem ();
+        }
+    }
+
+    private int GetMaxLengthItem ()
+    {
+        if (_source is null || _source?.Count == 0)
+        {
+            return 0;
+        }
+
+        var maxLength = 0;
+
+        for (var i = 0; i < _source!.Count; i++)
+        {
+            object? t = _source [i];
+
+            if (t is null)
+            {
+                continue;
+            }
+
+            int l;
+
+            l = t is string u ? u.GetColumns () : t.ToString ()!.Length;
+
+            if (l > maxLength)
+            {
+                maxLength = l;
+            }
+        }
+
+        return maxLength;
+    }
+
+    private static void RenderString (View driver, string str, int col, int line, int width, int viewportX = 0)
+    {
+        if (string.IsNullOrEmpty (str) || viewportX >= str.GetColumns ())
+        {
+            // Empty string or viewport beyond string - just fill with spaces
+            for (var i = 0; i < width; i++)
+            {
+                driver.AddRune ((Rune)' ');
+            }
+
+            return;
+        }
+
+        int runeLength = str.ToRunes ().Length;
+        int startIndex = Math.Min (viewportX, Math.Max (0, runeLength - 1));
+        string substring = str.Substring (startIndex);
+        string u = TextFormatter.ClipAndJustify (substring, width, Alignment.Start);
+        driver.AddStr (u);
+        width -= u.GetColumns ();
+
+        while (width-- > 0)
+        {
+            driver.AddRune ((Rune)' ');
+        }
+    }
+
+    private void Source_CollectionChanged (object? sender, NotifyCollectionChangedEventArgs e)
+    {
+        if (!SuspendCollectionChangedEvent)
+        {
+            CheckAndResizeMarksIfRequired ();
+            CollectionChanged?.Invoke (sender, e);
+        }
+    }
+}

+ 3 - 3
Terminal.Gui/Views/TableView/TableView.cs

@@ -1607,11 +1607,11 @@ public class TableView : View, IDesignable
             return false;
         }
 
-        int match = CollectionNavigator.GetNextMatchingItem (row, (char)key);
+        int? match = CollectionNavigator.GetNextMatchingItem (row, (char)key);
 
-        if (match != -1)
+        if (match != null)
         {
-            SelectedRow = match;
+            SelectedRow = match.Value;
             EnsureValidSelection ();
             EnsureSelectedCellIsVisible ();
             SetNeedsDraw ();

+ 1 - 1
Terminal.Gui/Views/Toplevel.cs

@@ -82,7 +82,7 @@ public partial class Toplevel : View
 
     // TODO: IRunnable: Re-implement as a property on IRunnable
     /// <summary>Gets or sets whether the main loop for this <see cref="Toplevel"/> is running or not.</summary>
-    /// <remarks>Setting this property directly is discouraged. Use <see cref="IApplication.RequestStop"/> instead.</remarks>
+    /// <remarks>Setting this property directly is discouraged. Use <see cref="IApplication.RequestStop()"/> instead.</remarks>
     public bool Running { get; set; }
 
     // TODO: IRunnable: Re-implement in IRunnable

+ 1 - 0
Terminal.sln.DotSettings

@@ -421,6 +421,7 @@
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=langword/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=Mazing/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=ogonek/@EntryIndexedValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=Quattro/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=Roslynator/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=Toplevel/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=Toplevels/@EntryIndexedValue">True</s:Boolean>

+ 2 - 2
Tests/IntegrationTests/UICatalog/ScenarioTests.cs

@@ -317,7 +317,7 @@ public class ScenarioTests : TestsAllViews
                                                      hostPane.FillRect (hostPane.Viewport);
                                                  }
 
-                                                 curView = CreateClass (viewClasses.Values.ToArray () [classListView.SelectedItem]);
+                                                 curView = CreateClass (viewClasses.Values.ToArray () [classListView.SelectedItem!.Value]);
                                              };
 
         xOptionSelector.ValueChanged += (_, _) => DimPosChanged (curView);
@@ -404,7 +404,7 @@ public class ScenarioTests : TestsAllViews
                 {
                     Assert.Equal (
                                   curView.GetType ().Name,
-                                  viewClasses.Values.ToArray () [classListView.SelectedItem].Name);
+                                  viewClasses.Values.ToArray () [classListView.SelectedItem!.Value].Name);
                 }
             }
             else

+ 1 - 1
Tests/TerminalGuiFluentTesting/GuiTestContext.ContextMenu.cs

@@ -24,7 +24,7 @@ public partial class GuiTestContext
                                    {
                                        // Registering with the PopoverManager will ensure that the context menu is closed when the view is no longer focused
                                        // and the context menu is disposed when it is closed.
-                                       App.Popover?.Register (contextMenu);
+                                       App?.Popover?.Register (contextMenu);
                                        contextMenu?.MakeVisible (e.ScreenPosition);
                                    }
                                };

+ 2 - 2
Tests/TerminalGuiFluentTesting/GuiTestContext.ViewBase.cs

@@ -28,11 +28,11 @@ public partial class GuiTestContext
     /// <summary>
     ///     The last view added (e.g. with <see cref="Add"/>) or the root/current top.
     /// </summary>
-    public View LastView => _lastView ?? App.Current ?? throw new ("Could not determine which view to add to");
+    public View LastView => _lastView ?? App?.Current ?? throw new ("Could not determine which view to add to");
 
     private T Find<T> (Func<T, bool> evaluator) where T : View
     {
-        Toplevel? t = App.Current;
+        Toplevel? t = App?.Current;
 
         if (t == null)
         {

+ 4 - 1
Tests/UnitTests/SetupFakeApplicationAttribute.cs

@@ -32,7 +32,10 @@ public class SetupFakeApplicationAttribute : BeforeAfterTestAttribute
 
         _appDispose?.Dispose ();
         _appDispose = null;
-        ApplicationImpl.SetInstance (null);
+
+        // TODO: This is troublesome; it seems to cause tests to hang when enabled, but shouldn't have any impact.
+        // TODO: Uncomment after investigation.
+        //ApplicationImpl.SetInstance (null);
 
         base.After (methodUnderTest);
     }

+ 0 - 1225
Tests/UnitTests/Views/ListViewTests.cs

@@ -1,1225 +0,0 @@
-using System.Collections;
-using System.Collections.ObjectModel;
-using System.Collections.Specialized;
-using Moq;
-using UnitTests;
-using Xunit.Abstractions;
-
-namespace UnitTests.ViewsTests;
-
-public class ListViewTests (ITestOutputHelper output)
-{
-    [Fact]
-    public void Constructors_Defaults ()
-    {
-        var lv = new ListView ();
-        Assert.Null (lv.Source);
-        Assert.True (lv.CanFocus);
-        Assert.Equal (-1, lv.SelectedItem);
-        Assert.False (lv.AllowsMultipleSelection);
-
-        lv = new () { Source = new ListWrapper<string> (["One", "Two", "Three"]) };
-        Assert.NotNull (lv.Source);
-        Assert.Equal (-1, lv.SelectedItem);
-
-        lv = new () { Source = new NewListDataSource () };
-        Assert.NotNull (lv.Source);
-        Assert.Equal (-1, lv.SelectedItem);
-
-        lv = new ()
-        {
-            Y = 1, Width = 10, Height = 20, Source = new ListWrapper<string> (["One", "Two", "Three"])
-        };
-        Assert.NotNull (lv.Source);
-        Assert.Equal (-1, lv.SelectedItem);
-        Assert.Equal (new (0, 1, 10, 20), lv.Frame);
-
-        lv = new () { Y = 1, Width = 10, Height = 20, Source = new NewListDataSource () };
-        Assert.NotNull (lv.Source);
-        Assert.Equal (-1, lv.SelectedItem);
-        Assert.Equal (new (0, 1, 10, 20), lv.Frame);
-
-    }
-
-    [Fact]
-    [AutoInitShutdown]
-    public void Ensures_Visibility_SelectedItem_On_MoveDown_And_MoveUp ()
-    {
-        ObservableCollection<string> source = [];
-
-        for (var i = 0; i < 20; i++)
-        {
-            source.Add ($"Line{i}");
-        }
-
-        var lv = new ListView { Width = Dim.Fill (), Height = Dim.Fill (), Source = new ListWrapper<string> (source) };
-        var win = new Window ();
-        win.Add (lv);
-        var top = new Toplevel ();
-        top.Add (win);
-        SessionToken rs = Application.Begin (top);
-        Application.Driver!.SetScreenSize (12, 12);
-        AutoInitShutdownAttribute.RunIteration ();
-
-        Assert.Equal (-1, lv.SelectedItem);
-
-        DriverAssert.AssertDriverContentsWithFrameAre (
-                                                      @"
-┌──────────┐
-│Line0     │
-│Line1     │
-│Line2     │
-│Line3     │
-│Line4     │
-│Line5     │
-│Line6     │
-│Line7     │
-│Line8     │
-│Line9     │
-└──────────┘",
-                                                      output
-                                                     );
-
-        Assert.True (lv.ScrollVertical (10));
-        AutoInitShutdownAttribute.RunIteration ();
-        Assert.Equal (-1, lv.SelectedItem);
-
-        DriverAssert.AssertDriverContentsWithFrameAre (
-                                                      @"
-┌──────────┐
-│Line10    │
-│Line11    │
-│Line12    │
-│Line13    │
-│Line14    │
-│Line15    │
-│Line16    │
-│Line17    │
-│Line18    │
-│Line19    │
-└──────────┘",
-                                                      output
-                                                     );
-
-        Assert.True (lv.MoveDown ());
-        AutoInitShutdownAttribute.RunIteration ();
-        Assert.Equal (0, lv.SelectedItem);
-
-        DriverAssert.AssertDriverContentsWithFrameAre (
-                                                      @"
-┌──────────┐
-│Line0     │
-│Line1     │
-│Line2     │
-│Line3     │
-│Line4     │
-│Line5     │
-│Line6     │
-│Line7     │
-│Line8     │
-│Line9     │
-└──────────┘",
-                                                      output
-                                                     );
-
-        Assert.True (lv.MoveEnd ());
-        AutoInitShutdownAttribute.RunIteration ();
-        Assert.Equal (19, lv.SelectedItem);
-
-        DriverAssert.AssertDriverContentsWithFrameAre (
-                                                      @"
-┌──────────┐
-│Line10    │
-│Line11    │
-│Line12    │
-│Line13    │
-│Line14    │
-│Line15    │
-│Line16    │
-│Line17    │
-│Line18    │
-│Line19    │
-└──────────┘",
-                                                      output
-                                                     );
-
-        Assert.True (lv.ScrollVertical (-20));
-        AutoInitShutdownAttribute.RunIteration ();
-        Assert.Equal (19, lv.SelectedItem);
-
-        DriverAssert.AssertDriverContentsWithFrameAre (
-                                                      @"
-┌──────────┐
-│Line0     │
-│Line1     │
-│Line2     │
-│Line3     │
-│Line4     │
-│Line5     │
-│Line6     │
-│Line7     │
-│Line8     │
-│Line9     │
-└──────────┘",
-                                                      output
-                                                     );
-
-        Assert.True (lv.MoveDown ());
-        AutoInitShutdownAttribute.RunIteration ();
-        Assert.Equal (19, lv.SelectedItem);
-
-        DriverAssert.AssertDriverContentsWithFrameAre (
-                                                      @"
-┌──────────┐
-│Line10    │
-│Line11    │
-│Line12    │
-│Line13    │
-│Line14    │
-│Line15    │
-│Line16    │
-│Line17    │
-│Line18    │
-│Line19    │
-└──────────┘",
-                                                      output
-                                                     );
-
-        Assert.True (lv.ScrollVertical (-20));
-        AutoInitShutdownAttribute.RunIteration ();
-        Assert.Equal (19, lv.SelectedItem);
-
-        DriverAssert.AssertDriverContentsWithFrameAre (
-                                                      @"
-┌──────────┐
-│Line0     │
-│Line1     │
-│Line2     │
-│Line3     │
-│Line4     │
-│Line5     │
-│Line6     │
-│Line7     │
-│Line8     │
-│Line9     │
-└──────────┘",
-                                                      output
-                                                     );
-
-        Assert.True (lv.MoveDown ());
-        AutoInitShutdownAttribute.RunIteration ();
-        Assert.Equal (19, lv.SelectedItem);
-
-        DriverAssert.AssertDriverContentsWithFrameAre (
-                                                      @"
-┌──────────┐
-│Line10    │
-│Line11    │
-│Line12    │
-│Line13    │
-│Line14    │
-│Line15    │
-│Line16    │
-│Line17    │
-│Line18    │
-│Line19    │
-└──────────┘",
-                                                      output
-                                                     );
-
-        Assert.True (lv.MoveHome ());
-        AutoInitShutdownAttribute.RunIteration ();
-        Assert.Equal (0, lv.SelectedItem);
-
-        DriverAssert.AssertDriverContentsWithFrameAre (
-                                                      @"
-┌──────────┐
-│Line0     │
-│Line1     │
-│Line2     │
-│Line3     │
-│Line4     │
-│Line5     │
-│Line6     │
-│Line7     │
-│Line8     │
-│Line9     │
-└──────────┘",
-                                                      output
-                                                     );
-
-        Assert.True (lv.ScrollVertical (20));
-        AutoInitShutdownAttribute.RunIteration ();
-        Assert.Equal (0, lv.SelectedItem);
-
-        DriverAssert.AssertDriverContentsWithFrameAre (
-                                                      @"
-┌──────────┐
-│Line19    │
-│          │
-│          │
-│          │
-│          │
-│          │
-│          │
-│          │
-│          │
-│          │
-└──────────┘",
-                                                      output
-                                                     );
-
-        Assert.True (lv.MoveUp ());
-        AutoInitShutdownAttribute.RunIteration ();
-        Assert.Equal (0, lv.SelectedItem);
-
-        DriverAssert.AssertDriverContentsWithFrameAre (
-                                                      @"
-┌──────────┐
-│Line0     │
-│Line1     │
-│Line2     │
-│Line3     │
-│Line4     │
-│Line5     │
-│Line6     │
-│Line7     │
-│Line8     │
-│Line9     │
-└──────────┘",
-                                                      output
-                                                     );
-        top.Dispose ();
-    }
-
-    [Fact]
-    [AutoInitShutdown]
-    public void EnsureSelectedItemVisible_SelectedItem ()
-    {
-        ObservableCollection<string> source = [];
-
-        for (var i = 0; i < 10; i++)
-        {
-            source.Add ($"Item {i}");
-        }
-
-        var lv = new ListView { Width = 10, Height = 5, Source = new ListWrapper<string> (source) };
-        var top = new Toplevel ();
-        top.Add (lv);
-        Application.Begin (top);
-        AutoInitShutdownAttribute.RunIteration ();
-
-        DriverAssert.AssertDriverContentsWithFrameAre (
-                                                      @"
-Item 0
-Item 1
-Item 2
-Item 3
-Item 4",
-                                                      output
-                                                     );
-
-        // EnsureSelectedItemVisible is auto enabled on the OnSelectedChanged
-        lv.SelectedItem = 6;
-        AutoInitShutdownAttribute.RunIteration ();
-
-        DriverAssert.AssertDriverContentsWithFrameAre (
-                                                      @"
-Item 2
-Item 3
-Item 4
-Item 5
-Item 6",
-                                                      output
-                                                     );
-        top.Dispose ();
-    }
-
-    [Fact]
-    [AutoInitShutdown]
-    public void EnsureSelectedItemVisible_Top ()
-    {
-        ObservableCollection<string> source = ["First", "Second"];
-        var lv = new ListView { Width = Dim.Fill (), Height = 1, Source = new ListWrapper<string> (source) };
-        lv.SelectedItem = 1;
-        var top = new Toplevel ();
-        top.Add (lv);
-        Application.Begin (top);
-        AutoInitShutdownAttribute.RunIteration ();
-
-        Assert.Equal ("Second ", GetContents (0));
-        Assert.Equal (new (' ', 7), GetContents (1));
-
-        lv.MoveUp ();
-        lv.Draw ();
-
-        Assert.Equal ("First  ", GetContents (0));
-        Assert.Equal (new (' ', 7), GetContents (1));
-
-        string GetContents (int line)
-        {
-            var item = "";
-
-            for (var i = 0; i < 7; i++)
-            {
-                item += Application.Driver?.Contents [line, i].Rune;
-            }
-
-            return item;
-        }
-        top.Dispose ();
-    }
-
-    [Fact]
-    public void KeyBindings_Command ()
-    {
-        ObservableCollection<string> source = ["One", "Two", "Three"];
-        var lv = new ListView { Height = 2, AllowsMarking = true, Source = new ListWrapper<string> (source) };
-        lv.BeginInit ();
-        lv.EndInit ();
-        Assert.Equal (-1, lv.SelectedItem);
-        Assert.True (lv.NewKeyDownEvent (Key.CursorDown));
-        Assert.Equal (0, lv.SelectedItem);
-        Assert.True (lv.NewKeyDownEvent (Key.CursorUp));
-        Assert.Equal (0, lv.SelectedItem);
-        Assert.True (lv.NewKeyDownEvent (Key.PageDown));
-        Assert.Equal (2, lv.SelectedItem);
-        Assert.Equal (2, lv.TopItem);
-        Assert.True (lv.NewKeyDownEvent (Key.PageUp));
-        Assert.Equal (0, lv.SelectedItem);
-        Assert.Equal (0, lv.TopItem);
-        Assert.False (lv.Source.IsMarked (lv.SelectedItem));
-        Assert.True (lv.NewKeyDownEvent (Key.Space));
-        Assert.True (lv.Source.IsMarked (lv.SelectedItem));
-        var opened = false;
-        lv.OpenSelectedItem += (s, _) => opened = true;
-        Assert.True (lv.NewKeyDownEvent (Key.Enter));
-        Assert.True (opened);
-        Assert.True (lv.NewKeyDownEvent (Key.End));
-        Assert.Equal (2, lv.SelectedItem);
-        Assert.True (lv.NewKeyDownEvent (Key.Home));
-        Assert.Equal (0, lv.SelectedItem);
-    }
-
-    [Fact]
-    public void HotKey_Command_SetsFocus ()
-    {
-        var view = new ListView ();
-
-        view.CanFocus = true;
-        Assert.False (view.HasFocus);
-        view.InvokeCommand (Command.HotKey);
-        Assert.True (view.HasFocus);
-    }
-
-    [Fact]
-    public void HotKey_Command_Does_Not_Accept ()
-    {
-        var listView = new ListView ();
-        var accepted = false;
-
-        listView.Accepting += OnAccepted;
-        listView.InvokeCommand (Command.HotKey);
-
-        Assert.False (accepted);
-
-        return;
-
-        void OnAccepted (object sender, CommandEventArgs e) { accepted = true; }
-    }
-
-    [Fact]
-    public void Accept_Command_Accepts_and_Opens_Selected_Item ()
-    {
-        ObservableCollection<string> source = ["One", "Two", "Three"];
-        var listView = new ListView { Source = new ListWrapper<string> (source) };
-        listView.SelectedItem = 0;
-
-        var accepted = false;
-        var opened = false;
-        var selectedValue = string.Empty;
-
-        listView.Accepting += Accepted;
-        listView.OpenSelectedItem += OpenSelectedItem;
-
-        listView.InvokeCommand (Command.Accept);
-
-        Assert.True (accepted);
-        Assert.True (opened);
-        Assert.Equal (source [0], selectedValue);
-
-        return;
-
-        void OpenSelectedItem (object sender, ListViewItemEventArgs e)
-        {
-            opened = true;
-            selectedValue = e.Value.ToString ();
-        }
-
-        void Accepted (object sender, CommandEventArgs e) { accepted = true; }
-    }
-
-    [Fact]
-    public void Accept_Cancel_Event_Prevents_OpenSelectedItem ()
-    {
-        ObservableCollection<string> source = ["One", "Two", "Three"];
-        var listView = new ListView { Source = new ListWrapper<string> (source) };
-        listView.SelectedItem = 0;
-
-        var accepted = false;
-        var opened = false;
-        var selectedValue = string.Empty;
-
-        listView.Accepting += Accepted;
-        listView.OpenSelectedItem += OpenSelectedItem;
-
-        listView.InvokeCommand (Command.Accept);
-
-        Assert.True (accepted);
-        Assert.False (opened);
-        Assert.Equal (string.Empty, selectedValue);
-
-        return;
-
-        void OpenSelectedItem (object sender, ListViewItemEventArgs e)
-        {
-            opened = true;
-            selectedValue = e.Value.ToString ();
-        }
-
-        void Accepted (object sender, CommandEventArgs e)
-        {
-            accepted = true;
-            e.Handled = true;
-        }
-    }
-
-    /// <summary>
-    ///     Tests that when none of the Commands in a chained keybinding are possible the
-    ///     <see cref="View.NewKeyDownEvent"/> returns the appropriate result
-    /// </summary>
-    [Fact]
-    public void ListViewProcessKeyReturnValue_WithMultipleCommands ()
-    {
-        var lv = new ListView { Source = new ListWrapper<string> (["One", "Two", "Three", "Four"]) };
-
-        Assert.NotNull (lv.Source);
-
-        // first item should be deselected by default
-        Assert.Equal (-1, lv.SelectedItem);
-
-        // bind shift down to move down twice in control
-        lv.KeyBindings.Add (Key.CursorDown.WithShift, Command.Down, Command.Down);
-
-        Key ev = Key.CursorDown.WithShift;
-
-        Assert.True (lv.NewKeyDownEvent (ev), "The first time we move down 2 it should be possible");
-
-        // After moving down twice from -1 we should be at 'Two'
-        Assert.Equal (1, lv.SelectedItem);
-
-        // clear the items
-        lv.SetSource<string> (null);
-
-        // Press key combo again - return should be false this time as none of the Commands are allowable
-        Assert.False (lv.NewKeyDownEvent (ev), "We cannot move down so will not respond to this");
-    }
-
-    [Fact]
-    public void AllowsMarking_True_SpaceWithShift_SelectsThenDown_SingleSelection ()
-    {
-        var lv = new ListView { Source = new ListWrapper<string> (["One", "Two", "Three"]) };
-        lv.AllowsMarking = true;
-        lv.AllowsMultipleSelection = false;
-
-        Assert.NotNull (lv.Source);
-
-        // first item should be deselected by default
-        Assert.Equal (-1, lv.SelectedItem);
-
-        // nothing is ticked
-        Assert.False (lv.Source.IsMarked (0));
-        Assert.False (lv.Source.IsMarked (1));
-        Assert.False (lv.Source.IsMarked (2));
-
-        // view should indicate that it has accepted and consumed the event
-        Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift));
-
-        // first item should now be selected
-        Assert.Equal (0, lv.SelectedItem);
-
-        // none of the items should be ticked
-        Assert.False (lv.Source.IsMarked (0));
-        Assert.False (lv.Source.IsMarked (1));
-        Assert.False (lv.Source.IsMarked (2));
-
-        // Press key combo again
-        Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift));
-
-        // second item should now be selected
-        Assert.Equal (1, lv.SelectedItem);
-
-        // first item only should be ticked
-        Assert.True (lv.Source.IsMarked (0));
-        Assert.False (lv.Source.IsMarked (1));
-        Assert.False (lv.Source.IsMarked (2));
-
-        // Press key combo again
-        Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift));
-        Assert.Equal (2, lv.SelectedItem);
-        Assert.False (lv.Source.IsMarked (0));
-        Assert.True (lv.Source.IsMarked (1));
-        Assert.False (lv.Source.IsMarked (2));
-
-        // Press key combo again
-        Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift));
-        Assert.Equal (2, lv.SelectedItem); // cannot move down any further
-        Assert.False (lv.Source.IsMarked (0));
-        Assert.False (lv.Source.IsMarked (1));
-        Assert.True (lv.Source.IsMarked (2)); // but can toggle marked
-
-        // Press key combo again 
-        Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift));
-        Assert.Equal (2, lv.SelectedItem); // cannot move down any further
-        Assert.False (lv.Source.IsMarked (0));
-        Assert.False (lv.Source.IsMarked (1));
-        Assert.False (lv.Source.IsMarked (2)); // untoggle toggle marked
-    }
-
-    [Fact]
-    public void AllowsMarking_True_SpaceWithShift_SelectsThenDown_MultipleSelection ()
-    {
-        var lv = new ListView { Source = new ListWrapper<string> (["One", "Two", "Three"]) };
-        lv.AllowsMarking = true;
-        lv.AllowsMultipleSelection = true;
-
-        Assert.NotNull (lv.Source);
-
-        // first item should be deselected by default
-        Assert.Equal (-1, lv.SelectedItem);
-
-        // nothing is ticked
-        Assert.False (lv.Source.IsMarked (0));
-        Assert.False (lv.Source.IsMarked (1));
-        Assert.False (lv.Source.IsMarked (2));
-
-        // view should indicate that it has accepted and consumed the event
-        Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift));
-
-        // first item should now be selected
-        Assert.Equal (0, lv.SelectedItem);
-
-        // none of the items should be ticked
-        Assert.False (lv.Source.IsMarked (0));
-        Assert.False (lv.Source.IsMarked (1));
-        Assert.False (lv.Source.IsMarked (2));
-
-        // Press key combo again
-        Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift));
-
-        // second item should now be selected
-        Assert.Equal (1, lv.SelectedItem);
-
-        // first item only should be ticked
-        Assert.True (lv.Source.IsMarked (0));
-        Assert.False (lv.Source.IsMarked (1));
-        Assert.False (lv.Source.IsMarked (2));
-
-        // Press key combo again
-        Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift));
-        Assert.Equal (2, lv.SelectedItem);
-        Assert.True (lv.Source.IsMarked (0));
-        Assert.True (lv.Source.IsMarked (1));
-        Assert.False (lv.Source.IsMarked (2));
-
-        // Press key combo again
-        Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift));
-        Assert.Equal (2, lv.SelectedItem); // cannot move down any further
-        Assert.True (lv.Source.IsMarked (0));
-        Assert.True (lv.Source.IsMarked (1));
-        Assert.True (lv.Source.IsMarked (2)); // but can toggle marked
-
-        // Press key combo again 
-        Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift));
-        Assert.Equal (2, lv.SelectedItem); // cannot move down any further
-        Assert.True (lv.Source.IsMarked (0));
-        Assert.True (lv.Source.IsMarked (1));
-        Assert.False (lv.Source.IsMarked (2)); // untoggle toggle marked
-    }
-
-    [Fact]
-    public void ListWrapper_StartsWith ()
-    {
-        var lw = new ListWrapper<string> (["One", "Two", "Three"]);
-
-        Assert.Equal (1, lw.StartsWith ("t"));
-        Assert.Equal (1, lw.StartsWith ("tw"));
-        Assert.Equal (2, lw.StartsWith ("th"));
-        Assert.Equal (1, lw.StartsWith ("T"));
-        Assert.Equal (1, lw.StartsWith ("TW"));
-        Assert.Equal (2, lw.StartsWith ("TH"));
-
-        lw = new (["One", "Two", "Three"]);
-
-        Assert.Equal (1, lw.StartsWith ("t"));
-        Assert.Equal (1, lw.StartsWith ("tw"));
-        Assert.Equal (2, lw.StartsWith ("th"));
-        Assert.Equal (1, lw.StartsWith ("T"));
-        Assert.Equal (1, lw.StartsWith ("TW"));
-        Assert.Equal (2, lw.StartsWith ("TH"));
-    }
-
-    [Fact]
-    public void OnEnter_Does_Not_Throw_Exception ()
-    {
-        var lv = new ListView ();
-        var top = new View ();
-        top.Add (lv);
-        Exception exception = Record.Exception (() => lv.SetFocus ());
-        Assert.Null (exception);
-    }
-
-    [Fact]
-    [AutoInitShutdown]
-    public void RowRender_Event ()
-    {
-        var rendered = false;
-        ObservableCollection<string> source = ["one", "two", "three"];
-        var lv = new ListView { Width = Dim.Fill (), Height = Dim.Fill () };
-        lv.RowRender += (s, _) => rendered = true;
-        var top = new Toplevel ();
-        top.Add (lv);
-        Application.Begin (top);
-        Assert.False (rendered);
-
-        lv.SetSource (source);
-        lv.Draw ();
-        Assert.True (rendered);
-        top.Dispose ();
-    }
-
-    [Fact]
-    public void SelectedItem_Get_Set ()
-    {
-        var lv = new ListView { Source = new ListWrapper<string> (["One", "Two", "Three"]) };
-        Assert.Equal (-1, lv.SelectedItem);
-        Assert.Throws<ArgumentException> (() => lv.SelectedItem = 3);
-        Exception exception = Record.Exception (() => lv.SelectedItem = -1);
-        Assert.Null (exception);
-    }
-
-    [Fact]
-    public void SetSource_Preserves_ListWrapper_Instance_If_Not_Null ()
-    {
-        var lv = new ListView { Source = new ListWrapper<string> (["One", "Two"]) };
-
-        Assert.NotNull (lv.Source);
-
-        lv.SetSource<string> (null);
-        Assert.NotNull (lv.Source);
-
-        lv.Source = null;
-        Assert.Null (lv.Source);
-
-        lv = new () { Source = new ListWrapper<string> (["One", "Two"]) };
-        Assert.NotNull (lv.Source);
-
-        lv.SetSourceAsync<string> (null);
-        Assert.NotNull (lv.Source);
-    }
-
-    [Fact]
-    public void SettingEmptyKeybindingThrows ()
-    {
-        var lv = new ListView { Source = new ListWrapper<string> (["One", "Two", "Three"]) };
-        Assert.Throws<ArgumentException> (() => lv.KeyBindings.Add (Key.Space));
-    }
-
-    private class NewListDataSource : IListDataSource
-    {
-#pragma warning disable CS0067
-        /// <inheritdoc />
-        public event NotifyCollectionChangedEventHandler CollectionChanged;
-#pragma warning restore CS0067
-
-        public int Count => 0;
-        public int Length => 0;
-
-        public bool SuspendCollectionChangedEvent { get => throw new NotImplementedException (); set => throw new NotImplementedException (); }
-
-        public bool IsMarked (int item) { throw new NotImplementedException (); }
-
-        public void Render (
-            ListView container,
-            bool selected,
-            int item,
-            int col,
-            int line,
-            int width,
-            int start = 0
-        )
-        {
-            throw new NotImplementedException ();
-        }
-
-        public void SetMark (int item, bool value) { throw new NotImplementedException (); }
-        public IList ToList () { return new List<string> { "One", "Two", "Three" }; }
-
-        public void Dispose ()
-        {
-            throw new NotImplementedException ();
-        }
-    }
-
-    [Fact]
-    [AutoInitShutdown]
-    public void Clicking_On_Border_Is_Ignored ()
-    {
-        var selected = "";
-
-        var lv = new ListView
-        {
-            Height = 5,
-            Width = 7,
-            BorderStyle = LineStyle.Single
-        };
-        lv.SetSource (["One", "Two", "Three", "Four"]);
-        lv.SelectedItemChanged += (s, e) => selected = e.Value.ToString ();
-        var top = new Toplevel ();
-        top.Add (lv);
-        Application.Begin (top);
-        AutoInitShutdownAttribute.RunIteration ();
-
-        Assert.Equal (new (1), lv.Border!.Thickness);
-        Assert.Equal (-1, lv.SelectedItem);
-        Assert.Equal ("", lv.Text);
-
-        DriverAssert.AssertDriverContentsWithFrameAre (
-                                                      @"
-┌─────┐
-│One  │
-│Two  │
-│Three│
-└─────┘",
-                                                      output);
-
-        Application.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.Button1Clicked });
-        Assert.Equal ("", selected);
-        Assert.Equal (-1, lv.SelectedItem);
-
-        Application.RaiseMouseEvent (
-                                  new ()
-                                  {
-                                      ScreenPosition = new (1, 1), Flags = MouseFlags.Button1Clicked
-                                  });
-        Assert.Equal ("One", selected);
-        Assert.Equal (0, lv.SelectedItem);
-
-        Application.RaiseMouseEvent (
-                                  new ()
-                                  {
-                                      ScreenPosition = new (1, 2), Flags = MouseFlags.Button1Clicked
-                                  });
-        Assert.Equal ("Two", selected);
-        Assert.Equal (1, lv.SelectedItem);
-
-        Application.RaiseMouseEvent (
-                                  new ()
-                                  {
-                                      ScreenPosition = new (1, 3), Flags = MouseFlags.Button1Clicked
-                                  });
-        Assert.Equal ("Three", selected);
-        Assert.Equal (2, lv.SelectedItem);
-
-        Application.RaiseMouseEvent (
-                                  new ()
-                                  {
-                                      ScreenPosition = new (1, 4), Flags = MouseFlags.Button1Clicked
-                                  });
-        Assert.Equal ("Three", selected);
-        Assert.Equal (2, lv.SelectedItem);
-        top.Dispose ();
-    }
-
-    [Fact]
-    [AutoInitShutdown]
-    public void LeftItem_TopItem_Tests ()
-    {
-        ObservableCollection<string> source = [];
-
-        for (int i = 0; i < 5; i++)
-        {
-            source.Add ($"Item {i}");
-        }
-
-        var lv = new ListView
-        {
-            X = 1,
-            Source = new ListWrapper<string> (source)
-        };
-        lv.Height = lv.Source.Count;
-        lv.Width = lv.MaxLength;
-        var top = new Toplevel ();
-        top.Add (lv);
-        Application.Begin (top);
-        AutoInitShutdownAttribute.RunIteration ();
-
-        DriverAssert.AssertDriverContentsWithFrameAre (
-                                                      @"
- Item 0
- Item 1
- Item 2
- Item 3
- Item 4",
-                                                      output);
-
-        lv.LeftItem = 1;
-        lv.TopItem = 1;
-        AutoInitShutdownAttribute.RunIteration ();
-
-        DriverAssert.AssertDriverContentsWithFrameAre (
-                                                      @"
- tem 1
- tem 2
- tem 3
- tem 4",
-                                                      output);
-        top.Dispose ();
-    }
-
-    [Fact]
-    public void CollectionChanged_Event ()
-    {
-        var added = 0;
-        var removed = 0;
-        ObservableCollection<string> source = [];
-        var lv = new ListView { Source = new ListWrapper<string> (source) };
-
-        lv.CollectionChanged += (sender, args) =>
-                                {
-                                    if (args.Action == NotifyCollectionChangedAction.Add)
-                                    {
-                                        added++;
-                                    }
-                                    else if (args.Action == NotifyCollectionChangedAction.Remove)
-                                    {
-                                        removed++;
-                                    }
-                                };
-
-        for (int i = 0; i < 3; i++)
-        {
-            source.Add ($"Item{i}");
-        }
-        Assert.Equal (3, added);
-        Assert.Equal (0, removed);
-
-        added = 0;
-
-        for (int i = 0; i < 3; i++)
-        {
-            source.Remove (source [0]);
-        }
-        Assert.Equal (0, added);
-        Assert.Equal (3, removed);
-        Assert.Empty (source);
-    }
-
-    [Fact]
-    public void CollectionChanged_Event_Is_Only_Subscribed_Once ()
-    {
-        var added = 0;
-        var removed = 0;
-        var otherActions = 0;
-        IList<string> source1 = [];
-        var lv = new ListView { Source = new ListWrapper<string> (new (source1)) };
-
-        lv.CollectionChanged += (sender, args) =>
-                                {
-                                    if (args.Action == NotifyCollectionChangedAction.Add)
-                                    {
-                                        added++;
-                                    }
-                                    else if (args.Action == NotifyCollectionChangedAction.Remove)
-                                    {
-                                        removed++;
-                                    }
-                                    else
-                                    {
-                                        otherActions++;
-                                    }
-                                };
-
-        ObservableCollection<string> source2 = [];
-        lv.Source = new ListWrapper<string> (source2);
-        ObservableCollection<string> source3 = [];
-        lv.Source = new ListWrapper<string> (source3);
-        Assert.Equal (0, added);
-        Assert.Equal (0, removed);
-        Assert.Equal (0, otherActions);
-
-        for (int i = 0; i < 3; i++)
-        {
-            source1.Add ($"Item{i}");
-            source2.Add ($"Item{i}");
-            source3.Add ($"Item{i}");
-        }
-        Assert.Equal (3, added);
-        Assert.Equal (0, removed);
-        Assert.Equal (0, otherActions);
-
-        added = 0;
-
-        for (int i = 0; i < 3; i++)
-        {
-            source1.Remove (source1 [0]);
-            source2.Remove (source2 [0]);
-            source3.Remove (source3 [0]);
-        }
-        Assert.Equal (0, added);
-        Assert.Equal (3, removed);
-        Assert.Equal (0, otherActions);
-        Assert.Empty (source1);
-        Assert.Empty (source2);
-        Assert.Empty (source3);
-    }
-
-    [Fact]
-    public void CollectionChanged_Event_UnSubscribe_Previous_If_New_Is_Null ()
-    {
-        var added = 0;
-        var removed = 0;
-        var otherActions = 0;
-        ObservableCollection<string> source1 = [];
-        var lv = new ListView { Source = new ListWrapper<string> (source1) };
-
-        lv.CollectionChanged += (sender, args) =>
-        {
-            if (args.Action == NotifyCollectionChangedAction.Add)
-            {
-                added++;
-            }
-            else if (args.Action == NotifyCollectionChangedAction.Remove)
-            {
-                removed++;
-            }
-            else
-            {
-                otherActions++;
-            }
-        };
-
-        lv.Source = new ListWrapper<string> (null);
-        Assert.Equal (0, added);
-        Assert.Equal (0, removed);
-        Assert.Equal (0, otherActions);
-
-        for (int i = 0; i < 3; i++)
-        {
-            source1.Add ($"Item{i}");
-        }
-        Assert.Equal (0, added);
-        Assert.Equal (0, removed);
-        Assert.Equal (0, otherActions);
-
-        for (int i = 0; i < 3; i++)
-        {
-            source1.Remove (source1 [0]);
-        }
-        Assert.Equal (0, added);
-        Assert.Equal (0, removed);
-        Assert.Equal (0, otherActions);
-        Assert.Empty (source1);
-    }
-
-    [Fact]
-    public void ListWrapper_CollectionChanged_Event_Is_Only_Subscribed_Once ()
-    {
-        var added = 0;
-        var removed = 0;
-        var otherActions = 0;
-        ObservableCollection<string> source1 = [];
-        ListWrapper<string> lw = new (source1);
-
-        lw.CollectionChanged += (sender, args) =>
-        {
-            if (args.Action == NotifyCollectionChangedAction.Add)
-            {
-                added++;
-            }
-            else if (args.Action == NotifyCollectionChangedAction.Remove)
-            {
-                removed++;
-            }
-            else
-            {
-                otherActions++;
-            }
-        };
-
-        ObservableCollection<string> source2 = [];
-        lw = new (source2);
-        ObservableCollection<string> source3 = [];
-        lw = new (source3);
-        Assert.Equal (0, added);
-        Assert.Equal (0, removed);
-        Assert.Equal (0, otherActions);
-
-        for (int i = 0; i < 3; i++)
-        {
-            source1.Add ($"Item{i}");
-            source2.Add ($"Item{i}");
-            source3.Add ($"Item{i}");
-        }
-
-        Assert.Equal (3, added);
-        Assert.Equal (0, removed);
-        Assert.Equal (0, otherActions);
-
-        added = 0;
-
-        for (int i = 0; i < 3; i++)
-        {
-            source1.Remove (source1 [0]);
-            source2.Remove (source2 [0]);
-            source3.Remove (source3 [0]);
-        }
-        Assert.Equal (0, added);
-        Assert.Equal (3, removed);
-        Assert.Equal (0, otherActions);
-        Assert.Empty (source1);
-        Assert.Empty (source2);
-        Assert.Empty (source3);
-    }
-
-    [Fact]
-    public void ListWrapper_CollectionChanged_Event_UnSubscribe_Previous_Is_Disposed ()
-    {
-        var added = 0;
-        var removed = 0;
-        var otherActions = 0;
-        ObservableCollection<string> source1 = [];
-        ListWrapper<string> lw = new (source1);
-
-        lw.CollectionChanged += Lw_CollectionChanged;
-
-        lw.Dispose ();
-        lw = new (null);
-        Assert.Equal (0, lw.Count);
-        Assert.Equal (0, added);
-        Assert.Equal (0, removed);
-        Assert.Equal (0, otherActions);
-
-        for (int i = 0; i < 3; i++)
-        {
-            source1.Add ($"Item{i}");
-        }
-        Assert.Equal (0, added);
-        Assert.Equal (0, removed);
-        Assert.Equal (0, otherActions);
-
-        for (int i = 0; i < 3; i++)
-        {
-            source1.Remove (source1 [0]);
-        }
-        Assert.Equal (0, added);
-        Assert.Equal (0, removed);
-        Assert.Equal (0, otherActions);
-        Assert.Empty (source1);
-
-
-        void Lw_CollectionChanged (object sender, NotifyCollectionChangedEventArgs e)
-        {
-            if (e.Action == NotifyCollectionChangedAction.Add)
-            {
-                added++;
-            }
-            else if (e.Action == NotifyCollectionChangedAction.Remove)
-            {
-                removed++;
-            }
-            else
-            {
-                otherActions++;
-            }
-        }
-    }
-
-    [Fact]
-    public void ListWrapper_SuspendCollectionChangedEvent_ResumeSuspendCollectionChangedEvent_Tests ()
-    {
-        var added = 0;
-        ObservableCollection<string> source = [];
-        ListWrapper<string> lw = new (source);
-
-        lw.CollectionChanged += Lw_CollectionChanged;
-
-        lw.SuspendCollectionChangedEvent = true;
-
-        for (int i = 0; i < 3; i++)
-        {
-            source.Add ($"Item{i}");
-        }
-        Assert.Equal (0, added);
-        Assert.Equal (3, lw.Count);
-        Assert.Equal (3, source.Count);
-
-        lw.SuspendCollectionChangedEvent = false;
-
-        for (int i = 3; i < 6; i++)
-        {
-            source.Add ($"Item{i}");
-        }
-        Assert.Equal (3, added);
-        Assert.Equal (6, lw.Count);
-        Assert.Equal (6, source.Count);
-
-
-        void Lw_CollectionChanged (object sender, NotifyCollectionChangedEventArgs e)
-        {
-            if (e.Action == NotifyCollectionChangedAction.Add)
-            {
-                added++;
-            }
-        }
-    }
-
-    [Fact]
-    public void ListView_SuspendCollectionChangedEvent_ResumeSuspendCollectionChangedEvent_Tests ()
-    {
-        var added = 0;
-        ObservableCollection<string> source = [];
-        ListView lv = new ListView { Source = new ListWrapper<string> (source) };
-
-        lv.CollectionChanged += Lw_CollectionChanged;
-
-        lv.SuspendCollectionChangedEvent ();
-
-        for (int i = 0; i < 3; i++)
-        {
-            source.Add ($"Item{i}");
-        }
-        Assert.Equal (0, added);
-        Assert.Equal (3, lv.Source.Count);
-        Assert.Equal (3, source.Count);
-
-        lv.ResumeSuspendCollectionChangedEvent ();
-
-        for (int i = 3; i < 6; i++)
-        {
-            source.Add ($"Item{i}");
-        }
-        Assert.Equal (3, added);
-        Assert.Equal (6, lv.Source.Count);
-        Assert.Equal (6, source.Count);
-
-
-        void Lw_CollectionChanged (object sender, NotifyCollectionChangedEventArgs e)
-        {
-            if (e.Action == NotifyCollectionChangedAction.Add)
-            {
-                added++;
-            }
-        }
-    }
-}

+ 11 - 11
Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs

@@ -7,7 +7,7 @@ public class SourcesManagerTests
     #region Update (Stream)
 
     [Fact]
-    public void Update_WithNullSettingsScope_ReturnsFalse ()
+    public void Load_WithNullSettingsScope_ReturnsFalse ()
     {
         // Arrange
         var sourcesManager = new SourcesManager ();
@@ -23,7 +23,7 @@ public class SourcesManagerTests
     }
 
     [Fact]
-    public void Update_WithValidStream_UpdatesSettingsScope ()
+    public void Load_WithValidStream_UpdatesSettingsScope ()
     {
         // Arrange
         var sourcesManager = new SourcesManager ();
@@ -56,7 +56,7 @@ public class SourcesManagerTests
     }
 
     [Fact]
-    public void Update_WithInvalidJson_AddsJsonError ()
+    public void Load_WithInvalidJson_AddsJsonError ()
     {
         // Arrange
         var sourcesManager = new SourcesManager ();
@@ -86,7 +86,7 @@ public class SourcesManagerTests
     #region Update (FilePath)
 
     [Fact]
-    public void Update_WithNonExistentFile_AddsToSourcesAndReturnsTrue ()
+    public void Load_WithNonExistentFile_AddsToSourcesAndReturnsTrue ()
     {
         // Arrange
         var sourcesManager = new SourcesManager ();
@@ -104,7 +104,7 @@ public class SourcesManagerTests
     }
 
     [Fact]
-    public void Update_WithValidFile_UpdatesSettingsScope ()
+    public void Load_WithValidFile_UpdatesSettingsScope ()
     {
         // Arrange
         var sourcesManager = new SourcesManager ();
@@ -140,7 +140,7 @@ public class SourcesManagerTests
     }
 
     [Fact]
-    public void Update_WithIOException_RetriesAndFailsGracefully ()
+    public void Load_WithIOException_RetriesAndFailsGracefully ()
     {
         // Arrange
         var sourcesManager = new SourcesManager ();
@@ -174,7 +174,7 @@ public class SourcesManagerTests
     #region Update (Json String)
 
     [Fact]
-    public void Update_WithNullOrEmptyJson_ReturnsFalse ()
+    public void Load_WithNullOrEmptyJson_ReturnsFalse ()
     {
         // Arrange
         var sourcesManager = new SourcesManager ();
@@ -193,7 +193,7 @@ public class SourcesManagerTests
     }
 
     [Fact]
-    public void Update_WithValidJson_UpdatesSettingsScope ()
+    public void Load_WithValidJson_UpdatesSettingsScope ()
     {
         // Arrange
         var sourcesManager = new SourcesManager ();
@@ -381,7 +381,7 @@ public class SourcesManagerTests
     }
 
     [Fact]
-    public void Update_WhenCalledMultipleTimes_MaintainsLastSourceForLocation ()
+    public void Load_WhenCalledMultipleTimes_MaintainsLastSourceForLocation ()
     {
         // Arrange
         var sourcesManager = new SourcesManager ();
@@ -401,7 +401,7 @@ public class SourcesManagerTests
     }
 
     [Fact]
-    public void Update_WithDifferentLocations_AddsAllSourcesToCollection ()
+    public void Load_WithDifferentLocations_AddsAllSourcesToCollection ()
     {
         // Arrange
         var sourcesManager = new SourcesManager ();
@@ -452,7 +452,7 @@ public class SourcesManagerTests
     }
 
     [Fact]
-    public void Update_WithNonExistentFileAndDifferentLocations_TracksAllSources ()
+    public void Load_WithNonExistentFileAndDifferentLocations_TracksAllSources ()
     {
         // Arrange
         var sourcesManager = new SourcesManager ();

+ 15 - 15
Tests/UnitTestsParallelizable/Text/CollectionNavigatorTests.cs

@@ -42,7 +42,7 @@ public class CollectionNavigatorTests
 
         // cycling with 'a'
         n = new CollectionNavigator (simpleStrings);
-        Assert.Equal (0, n.GetNextMatchingItem (-1, 'a'));
+        Assert.Equal (0, n.GetNextMatchingItem (null, 'a'));
         Assert.Equal (1, n.GetNextMatchingItem (0, 'a'));
 
         // if 4 (candle) is selected it should loop back to apricot
@@ -53,7 +53,7 @@ public class CollectionNavigatorTests
     public void Delay ()
     {
         var strings = new [] { "$$", "$100.00", "$101.00", "$101.10", "$200.00", "apricot" };
-        var current = 0;
+        int? current = 0;
         var n = new CollectionNavigator (strings);
 
         // No delay
@@ -96,7 +96,7 @@ public class CollectionNavigatorTests
         var strings = new [] { "apricot", "arm", "ta", "target", "text", "egg", "candle" };
 
         var n = new CollectionNavigator (strings);
-        var current = 0;
+        int? current = 0;
         Assert.Equal (strings.IndexOf ("ta"), current = n.GetNextMatchingItem (current, 't'));
 
         // should match "te" in "text"
@@ -137,7 +137,7 @@ public class CollectionNavigatorTests
     public void MinimizeMovement_False_ShouldMoveIfMultipleMatches ()
     {
         var strings = new [] { "$$", "$100.00", "$101.00", "$101.10", "$200.00", "apricot", "c", "car", "cart" };
-        var current = 0;
+        int? current = 0;
         var n = new CollectionNavigator (strings);
         Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$"));
         Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$"));
@@ -166,14 +166,14 @@ public class CollectionNavigatorTests
         Assert.Equal (strings.IndexOf ("car"), current = n.GetNextMatchingItem (current, "car"));
         Assert.Equal (strings.IndexOf ("cart"), current = n.GetNextMatchingItem (current, "car"));
 
-        Assert.Equal (-1, current = n.GetNextMatchingItem (current, "x"));
+        Assert.Null (n.GetNextMatchingItem (current, "x"));
     }
 
     [Fact]
     public void MinimizeMovement_True_ShouldStayOnCurrentIfMultipleMatches ()
     {
         var strings = new [] { "$$", "$100.00", "$101.00", "$101.10", "$200.00", "apricot", "c", "car", "cart" };
-        var current = 0;
+        int? current = 0;
         var n = new CollectionNavigator (strings);
         Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$", true));
         Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$", true));
@@ -185,14 +185,14 @@ public class CollectionNavigatorTests
         Assert.Equal (strings.IndexOf ("car"), current = n.GetNextMatchingItem (current, "car", true));
         Assert.Equal (strings.IndexOf ("car"), current = n.GetNextMatchingItem (current, "car", true));
 
-        Assert.Equal (-1, current = n.GetNextMatchingItem (current, "x", true));
+        Assert.Null (n.GetNextMatchingItem (current, "x", true));
     }
 
     [Fact]
     public void MutliKeySearchPlusWrongKeyStays ()
     {
         var strings = new [] { "a", "c", "can", "candle", "candy", "yellow", "zebra" };
-        var current = 0;
+        int? current = 0;
         var n = new CollectionNavigator (strings);
 
         // https://github.com/gui-cs/Terminal.Gui/pull/2132#issuecomment-1298425573
@@ -240,20 +240,20 @@ public class CollectionNavigatorTests
     }
 
     [Fact]
-    public void ShouldAcceptNegativeOne ()
+    public void ShouldAcceptNull ()
     {
         var n = new CollectionNavigator (simpleStrings);
 
-        // Expect that index of -1 (i.e. no selection) should work correctly
+        // Expect that index of null (i.e. no selection) should work correctly
         // and select the first entry of the letter 'b'
-        Assert.Equal (2, n.GetNextMatchingItem (-1, 'b'));
+        Assert.Equal (2, n.GetNextMatchingItem (null, 'b'));
     }
 
     [Fact]
     public void Symbols ()
     {
         var strings = new [] { "$$", "$100.00", "$101.00", "$101.10", "$200.00", "apricot" };
-        var current = 0;
+        int? current = 0;
         var n = new CollectionNavigator (strings);
         Assert.Equal (strings.IndexOf ("apricot"), current = n.GetNextMatchingItem (current, 'a'));
         Assert.Equal ("a", n.SearchString);
@@ -293,7 +293,7 @@ public class CollectionNavigatorTests
         var strings = new [] { "apricot", "arm", "ta", "丗丙业丞", "丗丙丛", "text", "egg", "candle" };
 
         var n = new CollectionNavigator (strings);
-        var current = 0;
+        int? current = 0;
         Assert.Equal (strings.IndexOf ("丗丙业丞"), current = n.GetNextMatchingItem (current, '丗'));
 
         // 丗丙业丞 is as good a match as 丗丙丛
@@ -319,7 +319,7 @@ public class CollectionNavigatorTests
     public void Word ()
     {
         var strings = new [] { "apricot", "arm", "bat", "batman", "bates hotel", "candle" };
-        var current = 0;
+        int? current = 0;
         var n = new CollectionNavigator (strings);
         Assert.Equal (strings.IndexOf ("bat"), current = n.GetNextMatchingItem (current, 'b')); // match bat
         Assert.Equal (strings.IndexOf ("bat"), current = n.GetNextMatchingItem (current, 'a')); // match bat
@@ -344,7 +344,7 @@ public class CollectionNavigatorTests
     public void CustomMatcher_NeverMatches ()
     {
         var strings = new [] { "apricot", "arm", "bat", "batman", "bates hotel", "candle" };
-        var current = 0;
+        int? current = 0;
         var n = new CollectionNavigator (strings);
 
         var matchNone = new Mock<ICollectionNavigatorMatcher> ();

+ 5 - 3
Tests/UnitTestsParallelizable/UnitTests.Parallelizable.csproj

@@ -27,6 +27,11 @@
     <PropertyGroup Condition="'$(Configuration)'=='Release'">
         <Optimize>true</Optimize>
     </PropertyGroup>
+    <ItemGroup>
+      <Compile Remove="ViewBase\**" />
+      <EmbeddedResource Remove="ViewBase\**" />
+      <None Remove="ViewBase\**" />
+    </ItemGroup>
 
     <ItemGroup>
         <Compile Include="..\UnitTests\DriverAssert.cs" Link="DriverAssert.cs" />
@@ -69,7 +74,4 @@
         <Using Include="Terminal.Gui" />
         <Using Include="Xunit" />
     </ItemGroup>
-    <ItemGroup>
-      <Folder Include="ViewBase\" />
-    </ItemGroup>
 </Project>

+ 5 - 5
Tests/UnitTestsParallelizable/View/Draw/NeedsDrawTests.cs

@@ -130,14 +130,14 @@ public class NeedsDrawTests : FakeDriverBase
         Assert.False (view.NeedsLayout);
 
         // SRL won't change anything since the view frame wasn't changed
-        view.SetRelativeLayout (Application.Screen.Size);
+        view.SetRelativeLayout (new (100, 100));
         Assert.False (view.NeedsDraw);
 
         view.SetNeedsLayout ();
 
         // SRL won't change anything since the view frame wasn't changed
         // SRL doesn't depend on NeedsLayout, but LayoutSubViews does
-        view.SetRelativeLayout (Application.Screen.Size);
+        view.SetRelativeLayout (new (100, 100));
         Assert.False (view.NeedsDraw);
         Assert.True (view.NeedsLayout);
 
@@ -180,7 +180,7 @@ public class NeedsDrawTests : FakeDriverBase
         Assert.True (view.NeedsDraw);
         Assert.True (superView.NeedsDraw);
 
-        superView.SetRelativeLayout (Application.Screen.Size);
+        superView.SetRelativeLayout (new (100, 100));
         Assert.True (view.NeedsDraw);
         Assert.True (superView.NeedsDraw);
     }
@@ -216,7 +216,7 @@ public class NeedsDrawTests : FakeDriverBase
         view.EndInit ();
         Assert.True (view.NeedsDraw);
 
-        view.SetRelativeLayout (Application.Screen.Size);
+        view.SetRelativeLayout (new (100, 100));
         Assert.True (view.NeedsDraw);
 
         view.LayoutSubViews ();
@@ -235,7 +235,7 @@ public class NeedsDrawTests : FakeDriverBase
         view.EndInit ();
         Assert.True (view.NeedsDraw);
 
-        view.SetRelativeLayout (Application.Screen.Size);
+        view.SetRelativeLayout (new (100, 100));
         Assert.True (view.NeedsDraw);
 
         view.LayoutSubViews ();

+ 1 - 1
Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.PosTypes.cs

@@ -597,7 +597,7 @@ public partial class DimAutoTests
 
         // Without a subview, width should be 10
         // Without a subview, height should be 1
-        view.SetRelativeLayout (Application.Screen.Size);
+        view.SetRelativeLayout (new (100, 100));
         Assert.Equal (10, view.Frame.Width);
         Assert.Equal (1, view.Frame.Height);
 

+ 7 - 7
Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.cs

@@ -304,10 +304,10 @@ public partial class DimAutoTests (ITestOutputHelper output)
             Width = Auto (),
             Height = 1
         };
-        view.SetRelativeLayout (Application.Screen.Size);
+        view.SetRelativeLayout (new (100, 100));
         lastSize = view.Frame.Size;
         view.HotKeySpecifier = (Rune)'*';
-        view.SetRelativeLayout (Application.Screen.Size);
+        view.SetRelativeLayout (new (100, 100));
         Assert.NotEqual (lastSize, view.Frame.Size);
 
         view = new ()
@@ -316,10 +316,10 @@ public partial class DimAutoTests (ITestOutputHelper output)
             Width = Auto (),
             Height = 1
         };
-        view.SetRelativeLayout (Application.Screen.Size);
+        view.SetRelativeLayout (new (100, 100));
         lastSize = view.Frame.Size;
         view.Text = "*ABCD";
-        view.SetRelativeLayout (Application.Screen.Size);
+        view.SetRelativeLayout (new (100, 100));
         Assert.NotEqual (lastSize, view.Frame.Size);
     }
 
@@ -703,7 +703,7 @@ public partial class DimAutoTests (ITestOutputHelper output)
 
         view.Text = text;
 
-        view.SetRelativeLayout (Application.Screen.Size);
+        view.SetRelativeLayout (new (100, 100));
 
         Assert.Equal (new (expectedW, expectedH), view.Frame.Size);
     }
@@ -812,7 +812,7 @@ public partial class DimAutoTests (ITestOutputHelper output)
 
         view.Text = text;
 
-        view.SetRelativeLayout (Application.Screen.Size);
+        view.SetRelativeLayout (new (100, 100));
 
         Assert.Equal (new (expectedW, expectedH), view.Frame.Size);
     }
@@ -831,7 +831,7 @@ public partial class DimAutoTests (ITestOutputHelper output)
 
         view.Text = text;
 
-        view.SetRelativeLayout (Application.Screen.Size);
+        view.SetRelativeLayout (new (100, 100));
 
         Assert.Equal (new (expectedW, expectedH), view.Frame.Size);
     }

+ 5 - 5
Tests/UnitTestsParallelizable/View/Layout/FrameTests.cs

@@ -120,7 +120,7 @@ public class FrameTests
         Assert.True (view.NeedsLayout);
         view.Layout ();
         Assert.False (view.NeedsLayout);
-        Assert.Equal (Application.Screen, view.Frame);
+        Assert.Equal (new Size (2048, 2048), view.Frame.Size);
 
         view.Frame = Rectangle.Empty;
         Assert.Equal (Rectangle.Empty, view.Frame);
@@ -165,7 +165,7 @@ public class FrameTests
         Assert.Equal (Rectangle.Empty, v.Frame);
         v.Dispose ();
 
-        v = new() { Frame = frame };
+        v = new () { Frame = frame };
         Assert.Equal (frame, v.Frame);
 
         v.Frame = newFrame;
@@ -181,7 +181,7 @@ public class FrameTests
         Assert.Equal (Dim.Absolute (40), v.Height);
         v.Dispose ();
 
-        v = new() { X = frame.X, Y = frame.Y, Text = "v" };
+        v = new () { X = frame.X, Y = frame.Y, Text = "v" };
         v.Frame = newFrame;
         Assert.Equal (newFrame, v.Frame);
 
@@ -196,7 +196,7 @@ public class FrameTests
         v.Dispose ();
 
         newFrame = new (10, 20, 30, 40);
-        v = new() { Frame = frame };
+        v = new () { Frame = frame };
         v.Frame = newFrame;
         Assert.Equal (newFrame, v.Frame);
 
@@ -210,7 +210,7 @@ public class FrameTests
         Assert.Equal (Dim.Absolute (40), v.Height);
         v.Dispose ();
 
-        v = new() { X = frame.X, Y = frame.Y, Text = "v" };
+        v = new () { X = frame.X, Y = frame.Y, Text = "v" };
         v.Frame = newFrame;
         Assert.Equal (newFrame, v.Frame);
 

+ 6 - 6
Tests/UnitTestsParallelizable/View/SubviewTests.cs

@@ -14,11 +14,11 @@ public class SubViewTests
 
         super.SuperViewChanged += (s, e) =>
                               {
-                                    superRaisedCount++;
+                                  superRaisedCount++;
                               };
         sub.SuperViewChanged += (s, e) =>
                                 {
-                                    if (e.SuperView is {})
+                                    if (e.SuperView is { })
                                     {
                                         subRaisedCount++;
                                     }
@@ -266,14 +266,14 @@ public class SubViewTests
         superView.Add (subview1, subview2, subview3);
 
         superView.MoveSubViewTowardsEnd (subview2);
-        Assert.Equal (subview2, superView.SubViews.ToArray() [^1]);
+        Assert.Equal (subview2, superView.SubViews.ToArray () [^1]);
 
         superView.MoveSubViewTowardsEnd (subview1);
-        Assert.Equal (subview1, superView.SubViews.ToArray() [1]);
+        Assert.Equal (subview1, superView.SubViews.ToArray () [1]);
 
         // Already at end, what happens?
         superView.MoveSubViewTowardsEnd (subview2);
-        Assert.Equal (subview2, superView.SubViews.ToArray() [^1]);
+        Assert.Equal (subview2, superView.SubViews.ToArray () [^1]);
     }
 
     [Fact]
@@ -517,7 +517,7 @@ public class SubViewTests
             Assert.False (v2AddedToWin.CanFocus);
             Assert.False (svAddedTov1.CanFocus);
 
-            Application.LayoutAndDraw ();
+            top.Layout ();
         };
 
         winAddedToTop.Initialized += (s, e) =>

+ 513 - 0
Tests/UnitTestsParallelizable/Views/IListDataSourceTests.cs

@@ -0,0 +1,513 @@
+#nullable enable
+using System.Collections;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.Text;
+using Xunit.Abstractions;
+
+// ReSharper disable InconsistentNaming
+
+namespace UnitTests_Parallelizable.ViewTests;
+
+public class IListDataSourceTests (ITestOutputHelper output)
+{
+    private readonly ITestOutputHelper _output = output;
+
+    #region Concurrent Modification Tests
+
+    [Fact]
+    public void ListWrapper_SuspendAndModify_NoEventsUntilResume ()
+    {
+        ObservableCollection<string> source = ["Item1"];
+        ListWrapper<string> wrapper = new (source);
+        var eventCount = 0;
+
+        wrapper.CollectionChanged += (s, e) => eventCount++;
+
+        wrapper.SuspendCollectionChangedEvent = true;
+
+        source.Add ("Item2");
+        source.Add ("Item3");
+        source.RemoveAt (0);
+
+        Assert.Equal (0, eventCount);
+
+        wrapper.SuspendCollectionChangedEvent = false;
+
+        // Should have adjusted marks for the removals that happened while suspended
+        Assert.Equal (2, wrapper.Count);
+    }
+
+    #endregion
+
+    /// <summary>
+    ///     Test implementation of IListDataSource for testing custom implementations
+    /// </summary>
+    private class TestListDataSource : IListDataSource
+    {
+        private readonly List<string> _items = ["Custom Item 00", "Custom Item 01", "Custom Item 02"];
+        private readonly BitArray _marks = new (3);
+
+        public event NotifyCollectionChangedEventHandler? CollectionChanged;
+
+        public int Count => _items.Count;
+
+        public int Length => _items.Any () ? _items.Max (s => s?.Length ?? 0) : 0;
+
+        public bool SuspendCollectionChangedEvent { get; set; }
+
+        public bool IsMarked (int item)
+        {
+            if (item < 0 || item >= _items.Count)
+            {
+                return false;
+            }
+
+            return _marks [item];
+        }
+
+        public void SetMark (int item, bool value)
+        {
+            if (item >= 0 && item < _items.Count)
+            {
+                _marks [item] = value;
+            }
+        }
+
+        public void Render (ListView listView, bool selected, int item, int col, int line, int width, int viewportX = 0)
+        {
+            if (item < 0 || item >= _items.Count)
+            {
+                return;
+            }
+
+            listView.Move (col, line);
+            string text = _items [item] ?? "";
+
+            if (viewportX < text.Length)
+            {
+                text = text.Substring (viewportX);
+            }
+            else
+            {
+                text = "";
+            }
+
+            if (text.Length > width)
+            {
+                text = text.Substring (0, width);
+            }
+
+            listView.AddStr (text);
+
+            // Fill remaining width
+            for (int i = text.Length; i < width; i++)
+            {
+                listView.AddRune ((Rune)' ');
+            }
+        }
+
+        public IList ToList () { return _items; }
+
+        public void Dispose () { IsDisposed = true; }
+
+        public void AddItem (string item)
+        {
+            _items.Add (item);
+
+            // Resize marks
+            var newMarks = new BitArray (_items.Count);
+
+            for (var i = 0; i < Math.Min (_marks.Length, newMarks.Length); i++)
+            {
+                newMarks [i] = _marks [i];
+            }
+
+            if (!SuspendCollectionChangedEvent)
+            {
+                CollectionChanged?.Invoke (this, new (NotifyCollectionChangedAction.Add, item, _items.Count - 1));
+            }
+        }
+
+        public bool IsDisposed { get; private set; }
+    }
+
+    #region ListWrapper<T> Render Tests
+
+    [Fact]
+    public void ListWrapper_Render_NullItem_RendersEmpty ()
+    {
+        ObservableCollection<string?> source = [null, "Item2"];
+        ListWrapper<string?> wrapper = new (source);
+        var listView = new ListView { Width = 20, Height = 2 };
+        listView.BeginInit ();
+        listView.EndInit ();
+
+        // Render the null item (index 0)
+        wrapper.Render (listView, false, 0, 0, 0, 20);
+
+        // Should not throw and should render empty/spaces
+        Assert.Equal (2, wrapper.Count);
+    }
+
+    [Fact]
+    public void ListWrapper_Render_EmptyString_RendersSpaces ()
+    {
+        ObservableCollection<string> source = [""];
+        ListWrapper<string> wrapper = new (source);
+        var listView = new ListView { Width = 20, Height = 1 };
+        listView.BeginInit ();
+        listView.EndInit ();
+
+        wrapper.Render (listView, false, 0, 0, 0, 20);
+
+        Assert.Equal (1, wrapper.Count);
+        Assert.Equal (0, wrapper.Length); // Empty string has zero length
+    }
+
+    [Fact]
+    public void ListWrapper_Render_UnicodeText_CalculatesWidthCorrectly ()
+    {
+        ObservableCollection<string> source = ["Hello 你好", "Test"];
+        ListWrapper<string> wrapper = new (source);
+
+        // "Hello 你好" should be: "Hello " (6) + "你" (2) + "好" (2) = 10 columns
+        Assert.True (wrapper.Length >= 10);
+    }
+
+    [Fact]
+    public void ListWrapper_Render_LongString_ClipsToWidth ()
+    {
+        var longString = new string ('X', 100);
+        ObservableCollection<string> source = [longString];
+        ListWrapper<string> wrapper = new (source);
+        var listView = new ListView { Width = 20, Height = 1 };
+        listView.BeginInit ();
+        listView.EndInit ();
+
+        wrapper.Render (listView, false, 0, 0, 0, 10);
+
+        Assert.Equal (100, wrapper.Length);
+    }
+
+    [Fact]
+    public void ListWrapper_Render_WithViewportX_ScrollsHorizontally ()
+    {
+        ObservableCollection<string> source = ["0123456789ABCDEF"];
+        ListWrapper<string> wrapper = new (source);
+        var listView = new ListView { Width = 10, Height = 1 };
+        listView.BeginInit ();
+        listView.EndInit ();
+
+        // Render with horizontal scroll offset of 5
+        wrapper.Render (listView, false, 0, 0, 0, 10, 5);
+
+        // Should render "56789ABCDE" (starting at position 5)
+        Assert.Equal (16, wrapper.Length);
+    }
+
+    [Fact]
+    public void ListWrapper_Render_ViewportXBeyondLength_RendersEmpty ()
+    {
+        ObservableCollection<string> source = ["Short"];
+        ListWrapper<string> wrapper = new (source);
+        var listView = new ListView { Width = 20, Height = 1 };
+        listView.BeginInit ();
+        listView.EndInit ();
+
+        // Render with viewport beyond string length
+        wrapper.Render (listView, false, 0, 0, 0, 10, 100);
+
+        Assert.Equal (5, wrapper.Length);
+    }
+
+    [Fact]
+    public void ListWrapper_Render_ColAndLine_PositionsCorrectly ()
+    {
+        ObservableCollection<string> source = ["Item1", "Item2"];
+        ListWrapper<string> wrapper = new (source);
+        var listView = new ListView { Width = 20, Height = 5 };
+        listView.BeginInit ();
+        listView.EndInit ();
+
+        // Render at different positions
+        wrapper.Render (listView, false, 0, 2, 1, 10); // col=2, line=1
+        wrapper.Render (listView, false, 1, 0, 3, 10); // col=0, line=3
+
+        Assert.Equal (2, wrapper.Count);
+    }
+
+    [Fact]
+    public void ListWrapper_Render_WidthConstraint_FillsRemaining ()
+    {
+        ObservableCollection<string> source = ["Hi"];
+        ListWrapper<string> wrapper = new (source);
+        var listView = new ListView { Width = 20, Height = 1 };
+        listView.BeginInit ();
+        listView.EndInit ();
+
+        // Render "Hi" in width of 10 - should fill remaining 8 with spaces
+        wrapper.Render (listView, false, 0, 0, 0, 10);
+
+        Assert.Equal (2, wrapper.Length);
+    }
+
+    [Fact]
+    public void ListWrapper_Render_NonStringType_UsesToString ()
+    {
+        ObservableCollection<int> source = [42, 100, -5];
+        ListWrapper<int> wrapper = new (source);
+        var listView = new ListView { Width = 20, Height = 3 };
+        listView.BeginInit ();
+        listView.EndInit ();
+
+        wrapper.Render (listView, false, 0, 0, 0, 10);
+        wrapper.Render (listView, false, 1, 0, 1, 10);
+        wrapper.Render (listView, false, 2, 0, 2, 10);
+
+        Assert.Equal (3, wrapper.Count);
+        Assert.True (wrapper.Length >= 2); // "42" is 2 chars, "100" is 3 chars
+    }
+
+    #endregion
+
+    #region Custom IListDataSource Implementation Tests
+
+    [Fact]
+    public void CustomDataSource_AllMembers_WorkCorrectly ()
+    {
+        var customSource = new TestListDataSource ();
+        var listView = new ListView { Source = customSource, Width = 20, Height = 5 };
+
+        Assert.Equal (3, customSource.Count);
+        Assert.Equal (14, customSource.Length); // "Custom Item 00" is 14 chars
+
+        // Test marking
+        Assert.False (customSource.IsMarked (0));
+        customSource.SetMark (0, true);
+        Assert.True (customSource.IsMarked (0));
+        customSource.SetMark (0, false);
+        Assert.False (customSource.IsMarked (0));
+
+        // Test ToList
+        IList list = customSource.ToList ();
+        Assert.Equal (3, list.Count);
+        Assert.Equal ("Custom Item 00", list [0]);
+
+        // Test render doesn't throw
+        listView.BeginInit ();
+        listView.EndInit ();
+        Exception ex = Record.Exception (() => customSource.Render (listView, false, 0, 0, 0, 20));
+        Assert.Null (ex);
+    }
+
+    [Fact]
+    public void CustomDataSource_CollectionChanged_RaisedOnModification ()
+    {
+        var customSource = new TestListDataSource ();
+        var eventRaised = false;
+        NotifyCollectionChangedAction? action = null;
+
+        customSource.CollectionChanged += (s, e) =>
+                                          {
+                                              eventRaised = true;
+                                              action = e.Action;
+                                          };
+
+        customSource.AddItem ("New Item");
+
+        Assert.True (eventRaised);
+        Assert.Equal (NotifyCollectionChangedAction.Add, action);
+        Assert.Equal (4, customSource.Count);
+    }
+
+    [Fact]
+    public void CustomDataSource_SuspendCollectionChanged_SuppressesEvents ()
+    {
+        var customSource = new TestListDataSource ();
+        var eventCount = 0;
+
+        customSource.CollectionChanged += (s, e) => eventCount++;
+
+        customSource.SuspendCollectionChangedEvent = true;
+        customSource.AddItem ("Item 1");
+        customSource.AddItem ("Item 2");
+        Assert.Equal (0, eventCount); // No events raised
+
+        customSource.SuspendCollectionChangedEvent = false;
+        customSource.AddItem ("Item 3");
+        Assert.Equal (1, eventCount); // Event raised after resume
+    }
+
+    [Fact]
+    public void CustomDataSource_Dispose_CleansUp ()
+    {
+        var customSource = new TestListDataSource ();
+
+        customSource.Dispose ();
+
+        // After dispose, adding should not raise events (if implemented correctly)
+        customSource.AddItem ("New Item");
+
+        // The test source doesn't unsubscribe in dispose, but this shows the pattern
+        Assert.True (customSource.IsDisposed);
+    }
+
+    #endregion
+
+    #region Edge Cases
+
+    [Fact]
+    public void ListWrapper_EmptyCollection_PropertiesReturnZero ()
+    {
+        ObservableCollection<string> source = [];
+        ListWrapper<string> wrapper = new (source);
+
+        Assert.Equal (0, wrapper.Count);
+        Assert.Equal (0, wrapper.Length);
+    }
+
+    [Fact]
+    public void ListWrapper_NullSource_HandledGracefully ()
+    {
+        ListWrapper<string> wrapper = new (null);
+
+        Assert.Equal (0, wrapper.Count);
+        Assert.Equal (0, wrapper.Length);
+
+        // ToList should not throw
+        IList list = wrapper.ToList ();
+        Assert.Empty (list);
+    }
+
+    [Fact]
+    public void ListWrapper_IsMarked_OutOfBounds_ReturnsFalse ()
+    {
+        ObservableCollection<string> source = ["Item1"];
+        ListWrapper<string> wrapper = new (source);
+
+        Assert.False (wrapper.IsMarked (-1));
+        Assert.False (wrapper.IsMarked (1));
+        Assert.False (wrapper.IsMarked (100));
+    }
+
+    [Fact]
+    public void ListWrapper_SetMark_OutOfBounds_DoesNotThrow ()
+    {
+        ObservableCollection<string> source = ["Item1"];
+        ListWrapper<string> wrapper = new (source);
+
+        Exception ex = Record.Exception (() => wrapper.SetMark (-1, true));
+        Assert.Null (ex);
+
+        ex = Record.Exception (() => wrapper.SetMark (100, true));
+        Assert.Null (ex);
+    }
+
+    [Fact]
+    public void ListWrapper_CollectionShrinks_MarksAdjusted ()
+    {
+        ObservableCollection<string> source = ["Item1", "Item2", "Item3"];
+        ListWrapper<string> wrapper = new (source);
+
+        wrapper.SetMark (0, true);
+        wrapper.SetMark (2, true);
+
+        Assert.True (wrapper.IsMarked (0));
+        Assert.True (wrapper.IsMarked (2));
+
+        // Remove item 1 (middle item)
+        source.RemoveAt (1);
+
+        Assert.Equal (2, wrapper.Count);
+        Assert.True (wrapper.IsMarked (0)); // Still marked
+
+        // Item that was at index 2 is now at index 1
+    }
+
+    [Fact]
+    public void ListWrapper_CollectionGrows_MarksPreserved ()
+    {
+        ObservableCollection<string> source = ["Item1"];
+        ListWrapper<string> wrapper = new (source);
+
+        wrapper.SetMark (0, true);
+        Assert.True (wrapper.IsMarked (0));
+
+        source.Add ("Item2");
+        source.Add ("Item3");
+
+        Assert.Equal (3, wrapper.Count);
+        Assert.True (wrapper.IsMarked (0)); // Original mark preserved
+        Assert.False (wrapper.IsMarked (1));
+        Assert.False (wrapper.IsMarked (2));
+    }
+
+    [Fact]
+    public void ListWrapper_StartsWith_EmptyString_ReturnsFirst ()
+    {
+        ObservableCollection<string> source = ["Apple", "Banana", "Cherry"];
+        ListWrapper<string> wrapper = new (source);
+
+        // Searching for empty string might return -1 or 0 depending on implementation
+        int result = wrapper.StartsWith ("");
+        Assert.True (result == -1 || result == 0);
+    }
+
+    [Fact]
+    public void ListWrapper_StartsWith_NoMatch_ReturnsNegative ()
+    {
+        ObservableCollection<string> source = ["Apple", "Banana", "Cherry"];
+        ListWrapper<string> wrapper = new (source);
+
+        int result = wrapper.StartsWith ("Zebra");
+        Assert.Equal (-1, result);
+    }
+
+    [Fact]
+    public void ListWrapper_StartsWith_CaseInsensitive ()
+    {
+        ObservableCollection<string> source = ["Apple", "Banana", "Cherry"];
+        ListWrapper<string> wrapper = new (source);
+
+        Assert.Equal (0, wrapper.StartsWith ("app"));
+        Assert.Equal (0, wrapper.StartsWith ("APP"));
+        Assert.Equal (1, wrapper.StartsWith ("ban"));
+        Assert.Equal (1, wrapper.StartsWith ("BAN"));
+    }
+
+    [Fact]
+    public void ListWrapper_MaxLength_UpdatesOnCollectionChange ()
+    {
+        ObservableCollection<string> source = ["Hi"];
+        ListWrapper<string> wrapper = new (source);
+
+        Assert.Equal (2, wrapper.Length);
+
+        source.Add ("Very Long String Indeed");
+        Assert.Equal (23, wrapper.Length);
+
+        source.Clear ();
+        source.Add ("X");
+        Assert.Equal (1, wrapper.Length);
+    }
+
+    [Fact]
+    public void ListWrapper_Dispose_UnsubscribesFromCollectionChanged ()
+    {
+        ObservableCollection<string> source = ["Item1"];
+        ListWrapper<string> wrapper = new (source);
+
+        wrapper.CollectionChanged += (s, e) => { };
+
+        wrapper.Dispose ();
+
+        // After dispose, source changes should not raise wrapper events
+        source.Add ("Item2");
+
+        // The wrapper's event might still fire, but the wrapper won't propagate source events
+        // This depends on implementation
+    }
+
+    #endregion
+}

+ 1502 - 38
Tests/UnitTestsParallelizable/Views/ListViewTests.cs

@@ -1,32 +1,89 @@
-using System.Collections.ObjectModel;
+#nullable enable
+using System.Collections;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
 using Moq;
+using Terminal.Gui;
+using UnitTests;
+using Xunit;
+using Xunit.Abstractions;
+
+// ReSharper disable AccessToModifiedClosure
 
 namespace UnitTests_Parallelizable.ViewsTests;
 
-public class ListViewTests
+public class ListViewTests (ITestOutputHelper output)
 {
+    private readonly ITestOutputHelper _output = output;
+    [Fact]
+    public void CollectionNavigatorMatcher_KeybindingsOverrideNavigator ()
+    {
+        ObservableCollection<string> source = ["apricot", "arm", "bat", "batman", "bates hotel", "candle"];
+        var lv = new ListView { Source = new ListWrapper<string> (source) };
+
+        lv.SetFocus ();
+
+        lv.KeyBindings.Add (Key.B, Command.Down);
+
+        Assert.Null (lv.SelectedItem);
+
+        // Keys should be consumed to move down the navigation i.e. to apricot
+        Assert.True (lv.NewKeyDownEvent (Key.B));
+        Assert.Equal (0, lv.SelectedItem);
+
+        Assert.True (lv.NewKeyDownEvent (Key.B));
+        Assert.Equal (1, lv.SelectedItem);
+
+        // There is no keybinding for Key.C so it hits collection navigator i.e. we jump to candle
+        Assert.True (lv.NewKeyDownEvent (Key.C));
+        Assert.Equal (5, lv.SelectedItem);
+    }
+
+    [Fact]
+    public void ListView_CollectionNavigatorMatcher_KeybindingsOverrideNavigator ()
+    {
+        ObservableCollection<string> source = ["apricot", "arm", "bat", "batman", "bates hotel", "candle"];
+        var lv = new ListView { Source = new ListWrapper<string> (source) };
+
+        lv.SetFocus ();
+
+        lv.KeyBindings.Add (Key.B, Command.Down);
+
+        Assert.Null (lv.SelectedItem);
+
+        // Keys should be consumed to move down the navigation i.e. to apricot
+        Assert.True (lv.NewKeyDownEvent (Key.B));
+        Assert.Equal (0, lv.SelectedItem);
+
+        Assert.True (lv.NewKeyDownEvent (Key.B));
+        Assert.Equal (1, lv.SelectedItem);
+
+        // There is no keybinding for Key.C so it hits collection navigator i.e. we jump to candle
+        Assert.True (lv.NewKeyDownEvent (Key.C));
+        Assert.Equal (5, lv.SelectedItem);
+    }
+
     [Fact]
     public void ListViewCollectionNavigatorMatcher_DefaultBehaviour ()
     {
-        ObservableCollection<string> source = new () { "apricot", "arm", "bat", "batman", "bates hotel", "candle" };
-        ListView lv = new ListView { Source = new ListWrapper<string> (source) };
+        ObservableCollection<string> source = ["apricot", "arm", "bat", "batman", "bates hotel", "candle"];
+        var lv = new ListView { Source = new ListWrapper<string> (source) };
 
         // Keys are consumed during navigation
         Assert.True (lv.NewKeyDownEvent (Key.B));
         Assert.True (lv.NewKeyDownEvent (Key.A));
         Assert.True (lv.NewKeyDownEvent (Key.T));
 
-        Assert.Equal ("bat", (string)lv.Source.ToList () [lv.SelectedItem]);
+        Assert.Equal ("bat", (string)lv.Source.ToList () [lv.SelectedItem!.Value]!);
     }
 
     [Fact]
     public void ListViewCollectionNavigatorMatcher_IgnoreKeys ()
     {
-        ObservableCollection<string> source = new () { "apricot", "arm", "bat", "batman", "bates hotel", "candle" };
-        ListView lv = new ListView { Source = new ListWrapper<string> (source) };
-
+        ObservableCollection<string> source = ["apricot", "arm", "bat", "batman", "bates hotel", "candle"];
+        var lv = new ListView { Source = new ListWrapper<string> (source) };
 
-        var matchNone = new Mock<ICollectionNavigatorMatcher> ();
+        Mock<ICollectionNavigatorMatcher> matchNone = new ();
 
         matchNone.Setup (m => m.IsCompatibleKey (It.IsAny<Key> ()))
                  .Returns (false);
@@ -45,11 +102,10 @@ public class ListViewTests
     [Fact]
     public void ListViewCollectionNavigatorMatcher_OverrideMatching ()
     {
-        ObservableCollection<string> source = new () { "apricot", "arm", "bat", "batman", "bates hotel", "candle" };
-        ListView lv = new ListView { Source = new ListWrapper<string> (source) };
-
+        ObservableCollection<string> source = ["apricot", "arm", "bat", "batman", "bates hotel", "candle"];
+        var lv = new ListView { Source = new ListWrapper<string> (source) };
 
-        var matchNone = new Mock<ICollectionNavigatorMatcher> ();
+        Mock<ICollectionNavigatorMatcher> matchNone = new ();
 
         matchNone.Setup (m => m.IsCompatibleKey (It.IsAny<Key> ()))
                  .Returns (true);
@@ -59,6 +115,7 @@ public class ListViewTests
                  .Returns ((string s, object key) => s.StartsWith ('B') && key?.ToString () == "candle");
 
         lv.KeystrokeNavigator.Matcher = matchNone.Object;
+
         // Keys are consumed during navigation
         Assert.True (lv.NewKeyDownEvent (Key.B));
         Assert.Equal (5, lv.SelectedItem);
@@ -67,54 +124,1461 @@ public class ListViewTests
         Assert.True (lv.NewKeyDownEvent (Key.T));
         Assert.Equal (5, lv.SelectedItem);
 
-        Assert.Equal ("candle", (string)lv.Source.ToList () [lv.SelectedItem]);
+        Assert.Equal ("candle", (string)lv.Source.ToList () [lv.SelectedItem!.Value]!);
     }
 
+    #region ListView Tests (from ListViewTests.cs - parallelizable)
+
     [Fact]
-    public void ListView_CollectionNavigatorMatcher_KeybindingsOverrideNavigator ()
+    public void Constructors_Defaults ()
     {
-        ObservableCollection<string> source = new () { "apricot", "arm", "bat", "batman", "bates hotel", "candle" };
-        ListView lv = new ListView { Source = new ListWrapper<string> (source) };
+        var lv = new ListView ();
+        Assert.Null (lv.Source);
+        Assert.True (lv.CanFocus);
+        Assert.Null (lv.SelectedItem);
+        Assert.False (lv.AllowsMultipleSelection);
 
-        lv.SetFocus ();
+        lv = new () { Source = new ListWrapper<string> (["One", "Two", "Three"]) };
+        Assert.NotNull (lv.Source);
+        Assert.Null (lv.SelectedItem);
 
-        lv.KeyBindings.Add (Key.B, Command.Down);
+        lv = new () { Source = new NewListDataSource () };
+        Assert.NotNull (lv.Source);
+        Assert.Null (lv.SelectedItem);
 
-        Assert.Equal (-1, lv.SelectedItem);
+        lv = new ()
+        {
+            Y = 1, Width = 10, Height = 20, Source = new ListWrapper<string> (["One", "Two", "Three"])
+        };
+        Assert.NotNull (lv.Source);
+        Assert.Null (lv.SelectedItem);
+        Assert.Equal (new (0, 1, 10, 20), lv.Frame);
 
-        // Keys should be consumed to move down the navigation i.e. to apricot
-        Assert.True (lv.NewKeyDownEvent (Key.B));
+        lv = new () { Y = 1, Width = 10, Height = 20, Source = new NewListDataSource () };
+        Assert.NotNull (lv.Source);
+        Assert.Null (lv.SelectedItem);
+        Assert.Equal (new (0, 1, 10, 20), lv.Frame);
+    }
+
+    private class NewListDataSource : IListDataSource
+    {
+#pragma warning disable CS0067
+        public event NotifyCollectionChangedEventHandler? CollectionChanged;
+#pragma warning restore CS0067
+
+        public int Count => 0;
+        public int Length => 0;
+
+        public bool SuspendCollectionChangedEvent
+        {
+            get => throw new NotImplementedException ();
+            set => throw new NotImplementedException ();
+        }
+
+        public bool IsMarked (int item) { throw new NotImplementedException (); }
+
+        public void Render (
+            ListView container,
+            bool selected,
+            int item,
+            int col,
+            int line,
+            int width,
+            int viewportX = 0
+        )
+        {
+            throw new NotImplementedException ();
+        }
+
+        public void SetMark (int item, bool value) { throw new NotImplementedException (); }
+        public IList ToList () { return new List<string> { "One", "Two", "Three" }; }
+
+        public void Dispose () { throw new NotImplementedException (); }
+    }
+
+    [Fact]
+    public void KeyBindings_Command ()
+    {
+        ObservableCollection<string> source = ["One", "Two", "Three"];
+        var lv = new ListView { Height = 2, AllowsMarking = true, Source = new ListWrapper<string> (source) };
+        lv.BeginInit ();
+        lv.EndInit ();
+        Assert.Null (lv.SelectedItem);
+        Assert.True (lv.NewKeyDownEvent (Key.CursorDown));
+        Assert.Equal (0, lv.SelectedItem);
+        Assert.True (lv.NewKeyDownEvent (Key.CursorUp));
+        Assert.Equal (0, lv.SelectedItem);
+        Assert.True (lv.NewKeyDownEvent (Key.PageDown));
+        Assert.Equal (2, lv.SelectedItem);
+        Assert.Equal (2, lv.TopItem);
+        Assert.True (lv.NewKeyDownEvent (Key.PageUp));
         Assert.Equal (0, lv.SelectedItem);
+        Assert.Equal (0, lv.TopItem);
+        Assert.False (lv.Source.IsMarked (lv.SelectedItem!.Value));
+        Assert.True (lv.NewKeyDownEvent (Key.Space));
+        Assert.True (lv.Source.IsMarked (lv.SelectedItem!.Value));
+        var opened = false;
+        lv.OpenSelectedItem += (s, _) => opened = true;
+        Assert.True (lv.NewKeyDownEvent (Key.Enter));
+        Assert.True (opened);
+        Assert.True (lv.NewKeyDownEvent (Key.End));
+        Assert.Equal (2, lv.SelectedItem);
+        Assert.True (lv.NewKeyDownEvent (Key.Home));
+        Assert.Equal (0, lv.SelectedItem);
+    }
 
-        Assert.True (lv.NewKeyDownEvent (Key.B));
+    [Fact]
+    public void HotKey_Command_SetsFocus ()
+    {
+        var view = new ListView ();
+
+        view.CanFocus = true;
+        Assert.False (view.HasFocus);
+        view.InvokeCommand (Command.HotKey);
+        Assert.True (view.HasFocus);
+    }
+
+    [Fact]
+    public void HotKey_Command_Does_Not_Accept ()
+    {
+        var listView = new ListView ();
+        var accepted = false;
+
+        listView.Accepting += OnAccepted;
+        listView.InvokeCommand (Command.HotKey);
+
+        Assert.False (accepted);
+
+        return;
+
+        void OnAccepted (object? sender, CommandEventArgs e) { accepted = true; }
+    }
+
+    [Fact]
+    public void Accept_Command_Accepts_and_Opens_Selected_Item ()
+    {
+        ObservableCollection<string> source = ["One", "Two", "Three"];
+        var listView = new ListView { Source = new ListWrapper<string> (source) };
+        listView.SelectedItem = 0;
+
+        var accepted = false;
+        var opened = false;
+        var selectedValue = string.Empty;
+
+        listView.Accepting += Accepted;
+        listView.OpenSelectedItem += OpenSelectedItem;
+
+        listView.InvokeCommand (Command.Accept);
+
+        Assert.True (accepted);
+        Assert.True (opened);
+        Assert.Equal (source [0], selectedValue);
+
+        return;
+
+        void OpenSelectedItem (object? sender, ListViewItemEventArgs e)
+        {
+            opened = true;
+            selectedValue = e.Value!.ToString ();
+        }
+
+        void Accepted (object? sender, CommandEventArgs e) { accepted = true; }
+    }
+
+    [Fact]
+    public void Accept_Cancel_Event_Prevents_OpenSelectedItem ()
+    {
+        ObservableCollection<string> source = ["One", "Two", "Three"];
+        var listView = new ListView { Source = new ListWrapper<string> (source) };
+        listView.SelectedItem = 0;
+
+        var accepted = false;
+        var opened = false;
+        var selectedValue = string.Empty;
+
+        listView.Accepting += Accepted;
+        listView.OpenSelectedItem += OpenSelectedItem;
+
+        listView.InvokeCommand (Command.Accept);
+
+        Assert.True (accepted);
+        Assert.False (opened);
+        Assert.Equal (string.Empty, selectedValue);
+
+        return;
+
+        void OpenSelectedItem (object? sender, ListViewItemEventArgs e)
+        {
+            opened = true;
+            selectedValue = e.Value!.ToString ();
+        }
+
+        void Accepted (object? sender, CommandEventArgs e)
+        {
+            accepted = true;
+            e.Handled = true;
+        }
+    }
+
+    [Fact]
+    public void ListViewProcessKeyReturnValue_WithMultipleCommands ()
+    {
+        var lv = new ListView { Source = new ListWrapper<string> (["One", "Two", "Three", "Four"]) };
+
+        Assert.NotNull (lv.Source);
+
+        // first item should be deselected by default
+        Assert.Null (lv.SelectedItem);
+
+        // bind shift down to move down twice in control
+        lv.KeyBindings.Add (Key.CursorDown.WithShift, Command.Down, Command.Down);
+
+        Key ev = Key.CursorDown.WithShift;
+
+        Assert.True (lv.NewKeyDownEvent (ev), "The first time we move down 2 it should be possible");
+
+        // After moving down twice from null we should be at 'Two'
         Assert.Equal (1, lv.SelectedItem);
 
-        // There is no keybinding for Key.C so it hits collection navigator i.e. we jump to candle
-        Assert.True (lv.NewKeyDownEvent (Key.C));
-        Assert.Equal (5, lv.SelectedItem);
+        // clear the items
+        lv.SetSource<string> (null);
+
+        // Press key combo again - return should be false this time as none of the Commands are allowable
+        Assert.False (lv.NewKeyDownEvent (ev), "We cannot move down so will not respond to this");
     }
 
     [Fact]
-    public void CollectionNavigatorMatcher_KeybindingsOverrideNavigator ()
+    public void AllowsMarking_True_SpaceWithShift_SelectsThenDown_SingleSelection ()
     {
-        ObservableCollection<string> source = new () { "apricot", "arm", "bat", "batman", "bates hotel", "candle" };
-        ListView lv = new ListView { Source = new ListWrapper<string> (source) };
+        var lv = new ListView { Source = new ListWrapper<string> (["One", "Two", "Three"]) };
+        lv.AllowsMarking = true;
+        lv.AllowsMultipleSelection = false;
 
-        lv.SetFocus ();
+        Assert.NotNull (lv.Source);
 
-        lv.KeyBindings.Add (Key.B, Command.Down);
+        // first item should be deselected by default
+        Assert.Null (lv.SelectedItem);
 
-        Assert.Equal (-1, lv.SelectedItem);
+        // nothing is ticked
+        Assert.False (lv.Source.IsMarked (0));
+        Assert.False (lv.Source.IsMarked (1));
+        Assert.False (lv.Source.IsMarked (2));
 
-        // Keys should be consumed to move down the navigation i.e. to apricot
-        Assert.True (lv.NewKeyDownEvent (Key.B));
+        // view should indicate that it has accepted and consumed the event
+        Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift));
+
+        // first item should now be selected
         Assert.Equal (0, lv.SelectedItem);
 
-        Assert.True (lv.NewKeyDownEvent (Key.B));
+        // none of the items should be ticked
+        Assert.False (lv.Source.IsMarked (0));
+        Assert.False (lv.Source.IsMarked (1));
+        Assert.False (lv.Source.IsMarked (2));
+
+        // Press key combo again
+        Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift));
+
+        // second item should now be selected
         Assert.Equal (1, lv.SelectedItem);
 
-        // There is no keybinding for Key.C so it hits collection navigator i.e. we jump to candle
-        Assert.True (lv.NewKeyDownEvent (Key.C));
-        Assert.Equal (5, lv.SelectedItem);
+        // first item only should be ticked
+        Assert.True (lv.Source.IsMarked (0));
+        Assert.False (lv.Source.IsMarked (1));
+        Assert.False (lv.Source.IsMarked (2));
+
+        // Press key combo again
+        Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift));
+        Assert.Equal (2, lv.SelectedItem);
+        Assert.False (lv.Source.IsMarked (0));
+        Assert.True (lv.Source.IsMarked (1));
+        Assert.False (lv.Source.IsMarked (2));
+
+        // Press key combo again
+        Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift));
+        Assert.Equal (2, lv.SelectedItem); // cannot move down any further
+        Assert.False (lv.Source.IsMarked (0));
+        Assert.False (lv.Source.IsMarked (1));
+        Assert.True (lv.Source.IsMarked (2)); // but can toggle marked
+
+        // Press key combo again 
+        Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift));
+        Assert.Equal (2, lv.SelectedItem); // cannot move down any further
+        Assert.False (lv.Source.IsMarked (0));
+        Assert.False (lv.Source.IsMarked (1));
+        Assert.False (lv.Source.IsMarked (2)); // untoggle toggle marked
+    }
+
+    [Fact]
+    public void AllowsMarking_True_SpaceWithShift_SelectsThenDown_MultipleSelection ()
+    {
+        var lv = new ListView { Source = new ListWrapper<string> (["One", "Two", "Three"]) };
+        lv.AllowsMarking = true;
+        lv.AllowsMultipleSelection = true;
+
+        Assert.NotNull (lv.Source);
+
+        // first item should be deselected by default
+        Assert.Null (lv.SelectedItem);
+
+        // nothing is ticked
+        Assert.False (lv.Source.IsMarked (0));
+        Assert.False (lv.Source.IsMarked (1));
+        Assert.False (lv.Source.IsMarked (2));
+
+        // view should indicate that it has accepted and consumed the event
+        Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift));
+
+        // first item should now be selected
+        Assert.Equal (0, lv.SelectedItem);
+
+        // none of the items should be ticked
+        Assert.False (lv.Source.IsMarked (0));
+        Assert.False (lv.Source.IsMarked (1));
+        Assert.False (lv.Source.IsMarked (2));
+
+        // Press key combo again
+        Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift));
+
+        // second item should now be selected
+        Assert.Equal (1, lv.SelectedItem);
+
+        // first item only should be ticked
+        Assert.True (lv.Source.IsMarked (0));
+        Assert.False (lv.Source.IsMarked (1));
+        Assert.False (lv.Source.IsMarked (2));
+
+        // Press key combo again
+        Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift));
+        Assert.Equal (2, lv.SelectedItem);
+        Assert.True (lv.Source.IsMarked (0));
+        Assert.True (lv.Source.IsMarked (1));
+        Assert.False (lv.Source.IsMarked (2));
+
+        // Press key combo again
+        Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift));
+        Assert.Equal (2, lv.SelectedItem); // cannot move down any further
+        Assert.True (lv.Source.IsMarked (0));
+        Assert.True (lv.Source.IsMarked (1));
+        Assert.True (lv.Source.IsMarked (2)); // but can toggle marked
+
+        // Press key combo again 
+        Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift));
+        Assert.Equal (2, lv.SelectedItem); // cannot move down any further
+        Assert.True (lv.Source.IsMarked (0));
+        Assert.True (lv.Source.IsMarked (1));
+        Assert.False (lv.Source.IsMarked (2)); // untoggle toggle marked
+    }
+
+    [Fact]
+    public void ListWrapper_StartsWith ()
+    {
+        ListWrapper<string> lw = new (["One", "Two", "Three"]);
+
+        Assert.Equal (1, lw.StartsWith ("t"));
+        Assert.Equal (1, lw.StartsWith ("tw"));
+        Assert.Equal (2, lw.StartsWith ("th"));
+        Assert.Equal (1, lw.StartsWith ("T"));
+        Assert.Equal (1, lw.StartsWith ("TW"));
+        Assert.Equal (2, lw.StartsWith ("TH"));
+
+        lw = new (["One", "Two", "Three"]);
+
+        Assert.Equal (1, lw.StartsWith ("t"));
+        Assert.Equal (1, lw.StartsWith ("tw"));
+        Assert.Equal (2, lw.StartsWith ("th"));
+        Assert.Equal (1, lw.StartsWith ("T"));
+        Assert.Equal (1, lw.StartsWith ("TW"));
+        Assert.Equal (2, lw.StartsWith ("TH"));
+    }
+
+    [Fact]
+    public void OnEnter_Does_Not_Throw_Exception ()
+    {
+        var lv = new ListView ();
+        var top = new View ();
+        top.Add (lv);
+        Exception exception = Record.Exception (() => lv.SetFocus ());
+        Assert.Null (exception);
+    }
+
+    [Fact]
+    public void SelectedItem_Get_Set ()
+    {
+        var lv = new ListView { Source = new ListWrapper<string> (["One", "Two", "Three"]) };
+        Assert.Null (lv.SelectedItem);
+        Assert.Throws<ArgumentException> (() => lv.SelectedItem = 3);
+        Exception exception = Record.Exception (() => lv.SelectedItem = null);
+        Assert.Null (exception);
+    }
+
+    [Fact]
+    public void SetSource_Preserves_ListWrapper_Instance_If_Not_Null ()
+    {
+        var lv = new ListView { Source = new ListWrapper<string> (["One", "Two"]) };
+
+        Assert.NotNull (lv.Source);
+
+        lv.SetSource<string> (null);
+        Assert.NotNull (lv.Source);
+
+        lv.Source = null;
+        Assert.Null (lv.Source);
+
+        lv = new () { Source = new ListWrapper<string> (["One", "Two"]) };
+        Assert.NotNull (lv.Source);
+
+        lv.SetSourceAsync<string> (null);
+        Assert.NotNull (lv.Source);
+    }
+
+    [Fact]
+    public void SettingEmptyKeybindingThrows ()
+    {
+        var lv = new ListView { Source = new ListWrapper<string> (["One", "Two", "Three"]) };
+        Assert.Throws<ArgumentException> (() => lv.KeyBindings.Add (Key.Space));
+    }
+
+    [Fact]
+    public void CollectionChanged_Event ()
+    {
+        var added = 0;
+        var removed = 0;
+        ObservableCollection<string> source = [];
+        var lv = new ListView { Source = new ListWrapper<string> (source) };
+
+        lv.CollectionChanged += (sender, args) =>
+                                {
+                                    if (args.Action == NotifyCollectionChangedAction.Add)
+                                    {
+                                        added++;
+                                    }
+                                    else if (args.Action == NotifyCollectionChangedAction.Remove)
+                                    {
+                                        removed++;
+                                    }
+                                };
+
+        for (var i = 0; i < 3; i++)
+        {
+            source.Add ($"Item{i}");
+        }
+
+        Assert.Equal (3, added);
+        Assert.Equal (0, removed);
+
+        added = 0;
+
+        for (var i = 0; i < 3; i++)
+        {
+            source.Remove (source [0]);
+        }
+
+        Assert.Equal (0, added);
+        Assert.Equal (3, removed);
+        Assert.Empty (source);
+    }
+
+    [Fact]
+    public void CollectionChanged_Event_Is_Only_Subscribed_Once ()
+    {
+        var added = 0;
+        var removed = 0;
+        var otherActions = 0;
+        IList<string> source1 = [];
+        var lv = new ListView { Source = new ListWrapper<string> (new (source1)) };
+
+        lv.CollectionChanged += (sender, args) =>
+                                {
+                                    if (args.Action == NotifyCollectionChangedAction.Add)
+                                    {
+                                        added++;
+                                    }
+                                    else if (args.Action == NotifyCollectionChangedAction.Remove)
+                                    {
+                                        removed++;
+                                    }
+                                    else
+                                    {
+                                        otherActions++;
+                                    }
+                                };
+
+        ObservableCollection<string> source2 = [];
+        lv.Source = new ListWrapper<string> (source2);
+        ObservableCollection<string> source3 = [];
+        lv.Source = new ListWrapper<string> (source3);
+        Assert.Equal (0, added);
+        Assert.Equal (0, removed);
+        Assert.Equal (0, otherActions);
+
+        for (var i = 0; i < 3; i++)
+        {
+            source1.Add ($"Item{i}");
+            source2.Add ($"Item{i}");
+            source3.Add ($"Item{i}");
+        }
+
+        Assert.Equal (3, added);
+        Assert.Equal (0, removed);
+        Assert.Equal (0, otherActions);
+
+        added = 0;
+
+        for (var i = 0; i < 3; i++)
+        {
+            source1.Remove (source1 [0]);
+            source2.Remove (source2 [0]);
+            source3.Remove (source3 [0]);
+        }
+
+        Assert.Equal (0, added);
+        Assert.Equal (3, removed);
+        Assert.Equal (0, otherActions);
+        Assert.Empty (source1);
+        Assert.Empty (source2);
+        Assert.Empty (source3);
+    }
+
+    [Fact]
+    public void CollectionChanged_Event_UnSubscribe_Previous_If_New_Is_Null ()
+    {
+        var added = 0;
+        var removed = 0;
+        var otherActions = 0;
+        ObservableCollection<string> source1 = [];
+        var lv = new ListView { Source = new ListWrapper<string> (source1) };
+
+        lv.CollectionChanged += (sender, args) =>
+                                {
+                                    if (args.Action == NotifyCollectionChangedAction.Add)
+                                    {
+                                        added++;
+                                    }
+                                    else if (args.Action == NotifyCollectionChangedAction.Remove)
+                                    {
+                                        removed++;
+                                    }
+                                    else
+                                    {
+                                        otherActions++;
+                                    }
+                                };
+
+        lv.Source = new ListWrapper<string> (null);
+        Assert.Equal (0, added);
+        Assert.Equal (0, removed);
+        Assert.Equal (0, otherActions);
+
+        for (var i = 0; i < 3; i++)
+        {
+            source1.Add ($"Item{i}");
+        }
+
+        Assert.Equal (0, added);
+        Assert.Equal (0, removed);
+        Assert.Equal (0, otherActions);
+
+        for (var i = 0; i < 3; i++)
+        {
+            source1.Remove (source1 [0]);
+        }
+
+        Assert.Equal (0, added);
+        Assert.Equal (0, removed);
+        Assert.Equal (0, otherActions);
+        Assert.Empty (source1);
+    }
+
+    [Fact]
+    public void ListWrapper_CollectionChanged_Event_Is_Only_Subscribed_Once ()
+    {
+        var added = 0;
+        var removed = 0;
+        var otherActions = 0;
+        ObservableCollection<string> source1 = [];
+        ListWrapper<string> lw = new (source1);
+
+        lw.CollectionChanged += (sender, args) =>
+                                {
+                                    if (args.Action == NotifyCollectionChangedAction.Add)
+                                    {
+                                        added++;
+                                    }
+                                    else if (args.Action == NotifyCollectionChangedAction.Remove)
+                                    {
+                                        removed++;
+                                    }
+                                    else
+                                    {
+                                        otherActions++;
+                                    }
+                                };
+
+        ObservableCollection<string> source2 = [];
+        lw = new (source2);
+        ObservableCollection<string> source3 = [];
+        lw = new (source3);
+        Assert.Equal (0, added);
+        Assert.Equal (0, removed);
+        Assert.Equal (0, otherActions);
+
+        for (var i = 0; i < 3; i++)
+        {
+            source1.Add ($"Item{i}");
+            source2.Add ($"Item{i}");
+            source3.Add ($"Item{i}");
+        }
+
+        Assert.Equal (3, added);
+        Assert.Equal (0, removed);
+        Assert.Equal (0, otherActions);
+
+        added = 0;
+
+        for (var i = 0; i < 3; i++)
+        {
+            source1.Remove (source1 [0]);
+            source2.Remove (source2 [0]);
+            source3.Remove (source3 [0]);
+        }
+
+        Assert.Equal (0, added);
+        Assert.Equal (3, removed);
+        Assert.Equal (0, otherActions);
+        Assert.Empty (source1);
+        Assert.Empty (source2);
+        Assert.Empty (source3);
+    }
+
+    [Fact]
+    public void ListWrapper_CollectionChanged_Event_UnSubscribe_Previous_Is_Disposed ()
+    {
+        var added = 0;
+        var removed = 0;
+        var otherActions = 0;
+        ObservableCollection<string> source1 = [];
+        ListWrapper<string> lw = new (source1);
+
+        lw.CollectionChanged += Lw_CollectionChanged;
+
+        lw.Dispose ();
+        lw = new (null);
+        Assert.Equal (0, lw.Count);
+        Assert.Equal (0, added);
+        Assert.Equal (0, removed);
+        Assert.Equal (0, otherActions);
+
+        for (var i = 0; i < 3; i++)
+        {
+            source1.Add ($"Item{i}");
+        }
+
+        Assert.Equal (0, added);
+        Assert.Equal (0, removed);
+        Assert.Equal (0, otherActions);
+
+        for (var i = 0; i < 3; i++)
+        {
+            source1.Remove (source1 [0]);
+        }
+
+        Assert.Equal (0, added);
+        Assert.Equal (0, removed);
+        Assert.Equal (0, otherActions);
+        Assert.Empty (source1);
+
+        void Lw_CollectionChanged (object? sender, NotifyCollectionChangedEventArgs e)
+        {
+            if (e.Action == NotifyCollectionChangedAction.Add)
+            {
+                added++;
+            }
+        }
+    }
+
+    [Fact]
+    public void ListWrapper_SuspendCollectionChangedEvent_ResumeSuspendCollectionChangedEvent_Tests ()
+    {
+        var added = 0;
+        ObservableCollection<string> source = [];
+        ListWrapper<string> lw = new (source);
+
+        lw.CollectionChanged += Lw_CollectionChanged;
+
+        lw.SuspendCollectionChangedEvent = true;
+
+        for (var i = 0; i < 3; i++)
+        {
+            source.Add ($"Item{i}");
+        }
+
+        Assert.Equal (0, added);
+        Assert.Equal (3, lw.Count);
+        Assert.Equal (3, source.Count);
+
+        lw.SuspendCollectionChangedEvent = false;
+
+        for (var i = 3; i < 6; i++)
+        {
+            source.Add ($"Item{i}");
+        }
+
+        Assert.Equal (3, added);
+        Assert.Equal (6, lw.Count);
+        Assert.Equal (6, source.Count);
+
+        void Lw_CollectionChanged (object? sender, NotifyCollectionChangedEventArgs e)
+        {
+            if (e.Action == NotifyCollectionChangedAction.Add)
+            {
+                added++;
+            }
+        }
+    }
+
+    [Fact]
+    public void ListView_SuspendCollectionChangedEvent_ResumeSuspendCollectionChangedEvent_Tests ()
+    {
+        var added = 0;
+        ObservableCollection<string> source = [];
+        var lv = new ListView { Source = new ListWrapper<string> (source) };
+
+        lv.CollectionChanged += Lw_CollectionChanged;
+
+        lv.SuspendCollectionChangedEvent ();
+
+        for (var i = 0; i < 3; i++)
+        {
+            source.Add ($"Item{i}");
+        }
+
+        Assert.Equal (0, added);
+        Assert.Equal (3, lv.Source.Count);
+        Assert.Equal (3, source.Count);
+
+        lv.ResumeSuspendCollectionChangedEvent ();
+
+        for (var i = 3; i < 6; i++)
+        {
+            source.Add ($"Item{i}");
+        }
+
+        Assert.Equal (3, added);
+        Assert.Equal (6, lv.Source.Count);
+        Assert.Equal (6, source.Count);
+
+        void Lw_CollectionChanged (object? sender, NotifyCollectionChangedEventArgs e)
+        {
+            if (e.Action == NotifyCollectionChangedAction.Add)
+            {
+                added++;
+            }
+        }
+    }
+
+    #endregion
+
+    [Fact]
+    public void Clicking_On_Border_Is_Ignored ()
+    {
+        IApplication? app = Application.Create ();
+        app.Init ("fake");
+
+        var selected = "";
+
+        var lv = new ListView
+        {
+            Height = 5,
+            Width = 7,
+            BorderStyle = LineStyle.Single
+        };
+        lv.SetSource (["One", "Two", "Three", "Four"]);
+        lv.SelectedItemChanged += (s, e) => selected = e.Value.ToString ();
+        var top = new Toplevel ();
+        top.Add (lv);
+        app.Begin (top);
+
+        //AutoInitShutdownAttribute.RunIteration ();
+
+        Assert.Equal (new (1), lv.Border!.Thickness);
+        Assert.Null (lv.SelectedItem);
+        Assert.Equal ("", lv.Text);
+
+        DriverAssert.AssertDriverContentsWithFrameAre (
+                                                       @"
+┌─────┐
+│One  │
+│Two  │
+│Three│
+└─────┘",
+                                                       _output, app?.Driver);
+
+        app?.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.Button1Clicked });
+        Assert.Equal ("", selected);
+        Assert.Null (lv.SelectedItem);
+
+        app?.Mouse.RaiseMouseEvent (
+                                    new ()
+                                    {
+                                        ScreenPosition = new (1, 1), Flags = MouseFlags.Button1Clicked
+                                    });
+        Assert.Equal ("One", selected);
+        Assert.Equal (0, lv.SelectedItem);
+
+        app?.Mouse.RaiseMouseEvent (
+                                    new ()
+                                    {
+                                        ScreenPosition = new (1, 2), Flags = MouseFlags.Button1Clicked
+                                    });
+        Assert.Equal ("Two", selected);
+        Assert.Equal (1, lv.SelectedItem);
+
+        app?.Mouse.RaiseMouseEvent (
+                                    new ()
+                                    {
+                                        ScreenPosition = new (1, 3), Flags = MouseFlags.Button1Clicked
+                                    });
+        Assert.Equal ("Three", selected);
+        Assert.Equal (2, lv.SelectedItem);
+
+        app?.Mouse.RaiseMouseEvent (
+                                    new ()
+                                    {
+                                        ScreenPosition = new (1, 4), Flags = MouseFlags.Button1Clicked
+                                    });
+        Assert.Equal ("Three", selected);
+        Assert.Equal (2, lv.SelectedItem);
+        top.Dispose ();
+
+        app.Shutdown ();
+    }
+
+    [Fact]
+    public void Ensures_Visibility_SelectedItem_On_MoveDown_And_MoveUp ()
+    {
+        IApplication? app = Application.Create ();
+        app.Init ("fake");
+        app.Driver?.SetScreenSize (12, 12);
+
+        ObservableCollection<string> source = [];
+
+        for (var i = 0; i < 20; i++)
+        {
+            source.Add ($"Line{i}");
+        }
+
+        var lv = new ListView { Width = Dim.Fill (), Height = Dim.Fill (), Source = new ListWrapper<string> (source) };
+        var win = new Window ();
+        win.Add (lv);
+        var top = new Toplevel ();
+        top.Add (win);
+        app.Begin (top);
+
+        Assert.Null (lv.SelectedItem);
+
+        DriverAssert.AssertDriverContentsWithFrameAre (
+                                                       @"
+┌──────────┐
+│Line0     │
+│Line1     │
+│Line2     │
+│Line3     │
+│Line4     │
+│Line5     │
+│Line6     │
+│Line7     │
+│Line8     │
+│Line9     │
+└──────────┘",
+                                                       _output, app.Driver
+                                                      );
+
+        Assert.True (lv.ScrollVertical (10));
+        app.LayoutAndDraw ();
+        Assert.Null (lv.SelectedItem);
+
+        DriverAssert.AssertDriverContentsWithFrameAre (
+                                                       @"
+┌──────────┐
+│Line10    │
+│Line11    │
+│Line12    │
+│Line13    │
+│Line14    │
+│Line15    │
+│Line16    │
+│Line17    │
+│Line18    │
+│Line19    │
+└──────────┘",
+                                                       _output, app.Driver
+                                                      );
+
+        Assert.True (lv.MoveDown ());
+        app.LayoutAndDraw ();
+        Assert.Equal (0, lv.SelectedItem);
+
+        DriverAssert.AssertDriverContentsWithFrameAre (
+                                                       @"
+┌──────────┐
+│Line0     │
+│Line1     │
+│Line2     │
+│Line3     │
+│Line4     │
+│Line5     │
+│Line6     │
+│Line7     │
+│Line8     │
+│Line9     │
+└──────────┘",
+                                                       _output, app.Driver
+                                                      );
+
+        Assert.True (lv.MoveEnd ());
+        app.LayoutAndDraw ();
+        Assert.Equal (19, lv.SelectedItem);
+
+        DriverAssert.AssertDriverContentsWithFrameAre (
+                                                       @"
+┌──────────┐
+│Line10    │
+│Line11    │
+│Line12    │
+│Line13    │
+│Line14    │
+│Line15    │
+│Line16    │
+│Line17    │
+│Line18    │
+│Line19    │
+└──────────┘",
+                                                       _output, app.Driver
+                                                      );
+
+        Assert.True (lv.ScrollVertical (-20));
+        app.LayoutAndDraw ();
+        Assert.Equal (19, lv.SelectedItem);
+
+        DriverAssert.AssertDriverContentsWithFrameAre (
+                                                       @"
+┌──────────┐
+│Line0     │
+│Line1     │
+│Line2     │
+│Line3     │
+│Line4     │
+│Line5     │
+│Line6     │
+│Line7     │
+│Line8     │
+│Line9     │
+└──────────┘",
+                                                       _output, app.Driver
+                                                      );
+
+        Assert.True (lv.MoveDown ());
+        app.LayoutAndDraw ();
+        Assert.Equal (19, lv.SelectedItem);
+
+        DriverAssert.AssertDriverContentsWithFrameAre (
+                                                       @"
+┌──────────┐
+│Line10    │
+│Line11    │
+│Line12    │
+│Line13    │
+│Line14    │
+│Line15    │
+│Line16    │
+│Line17    │
+│Line18    │
+│Line19    │
+└──────────┘",
+                                                       _output, app.Driver
+                                                      );
+
+        Assert.True (lv.ScrollVertical (-20));
+        app.LayoutAndDraw ();
+        Assert.Equal (19, lv.SelectedItem);
+
+        DriverAssert.AssertDriverContentsWithFrameAre (
+                                                       @"
+┌──────────┐
+│Line0     │
+│Line1     │
+│Line2     │
+│Line3     │
+│Line4     │
+│Line5     │
+│Line6     │
+│Line7     │
+│Line8     │
+│Line9     │
+└──────────┘",
+                                                       _output, app.Driver
+                                                      );
+
+        Assert.True (lv.MoveDown ());
+        app.LayoutAndDraw ();
+        Assert.Equal (19, lv.SelectedItem);
+
+        DriverAssert.AssertDriverContentsWithFrameAre (
+                                                       @"
+┌──────────┐
+│Line10    │
+│Line11    │
+│Line12    │
+│Line13    │
+│Line14    │
+│Line15    │
+│Line16    │
+│Line17    │
+│Line18    │
+│Line19    │
+└──────────┘",
+                                                       _output, app.Driver
+                                                      );
+
+        Assert.True (lv.MoveHome ());
+        app.LayoutAndDraw ();
+        Assert.Equal (0, lv.SelectedItem);
+
+        DriverAssert.AssertDriverContentsWithFrameAre (
+                                                       @"
+┌──────────┐
+│Line0     │
+│Line1     │
+│Line2     │
+│Line3     │
+│Line4     │
+│Line5     │
+│Line6     │
+│Line7     │
+│Line8     │
+│Line9     │
+└──────────┘",
+                                                       _output, app.Driver
+                                                      );
+
+        Assert.True (lv.ScrollVertical (20));
+        app.LayoutAndDraw ();
+        Assert.Equal (0, lv.SelectedItem);
+
+        DriverAssert.AssertDriverContentsWithFrameAre (
+                                                       @"
+┌──────────┐
+│Line19    │
+│          │
+│          │
+│          │
+│          │
+│          │
+│          │
+│          │
+│          │
+│          │
+└──────────┘",
+                                                       _output, app.Driver
+                                                      );
+
+        Assert.True (lv.MoveUp ());
+        app.LayoutAndDraw ();
+        Assert.Equal (0, lv.SelectedItem);
+
+        DriverAssert.AssertDriverContentsWithFrameAre (
+                                                       @"
+┌──────────┐
+│Line0     │
+│Line1     │
+│Line2     │
+│Line3     │
+│Line4     │
+│Line5     │
+│Line6     │
+│Line7     │
+│Line8     │
+│Line9     │
+└──────────┘",
+                                                       _output, app.Driver
+                                                      );
+        top.Dispose ();
+        app.Shutdown ();
+    }
+
+    [Fact]
+    public void EnsureSelectedItemVisible_SelectedItem ()
+    {
+        IApplication? app = Application.Create ();
+        app.Init ("fake");
+        app.Driver?.SetScreenSize (12, 12);
+
+        ObservableCollection<string> source = [];
+
+        for (var i = 0; i < 10; i++)
+        {
+            source.Add ($"Item {i}");
+        }
+
+        var lv = new ListView { Width = 10, Height = 5, Source = new ListWrapper<string> (source) };
+        var top = new Toplevel ();
+        top.Add (lv);
+        app.Begin (top);
+
+        DriverAssert.AssertDriverContentsWithFrameAre (
+                                                       @"
+Item 0
+Item 1
+Item 2
+Item 3
+Item 4",
+                                                       _output, app.Driver
+                                                      );
+
+        // EnsureSelectedItemVisible is auto enabled on the OnSelectedChanged
+        lv.SelectedItem = 6;
+        app.LayoutAndDraw ();
+
+        DriverAssert.AssertDriverContentsWithFrameAre (
+                                                       @"
+Item 2
+Item 3
+Item 4
+Item 5
+Item 6",
+                                                       _output, app.Driver
+                                                      );
+        top.Dispose ();
+        app.Shutdown ();
+    }
+
+    [Fact]
+    public void EnsureSelectedItemVisible_Top ()
+    {
+        IApplication? app = Application.Create ();
+        app.Init ("fake");
+        IDriver? driver = app.Driver;
+        driver.SetScreenSize (8, 2);
+
+        ObservableCollection<string> source = ["First", "Second"];
+        var lv = new ListView { Width = Dim.Fill (), Height = 1, Source = new ListWrapper<string> (source) };
+        lv.SelectedItem = 1;
+        var top = new Toplevel ();
+        top.Add (lv);
+        app.Begin (top);
+
+        Assert.Equal ("Second ", GetContents (0));
+        Assert.Equal (new (' ', 7), GetContents (1));
+
+        lv.MoveUp ();
+        lv.Draw ();
+
+        Assert.Equal ("First  ", GetContents (0));
+        Assert.Equal (new (' ', 7), GetContents (1));
+
+        string GetContents (int line)
+        {
+            var item = "";
+
+            for (var i = 0; i < 7; i++)
+            {
+                item += app.Driver?.Contents [line, i].Rune;
+            }
+
+            return item;
+        }
+
+        top.Dispose ();
+        app.Shutdown ();
+    }
+
+    [Fact]
+    public void LeftItem_TopItem_Tests ()
+    {
+        IApplication? app = Application.Create ();
+        app.Init ("fake");
+        app.Driver?.SetScreenSize (12, 12);
+
+        ObservableCollection<string> source = [];
+
+        for (var i = 0; i < 5; i++)
+        {
+            source.Add ($"Item {i}");
+        }
+
+        var lv = new ListView
+        {
+            X = 1,
+            Source = new ListWrapper<string> (source)
+        };
+        lv.Height = lv.Source.Count;
+        lv.Width = lv.MaxLength;
+        var top = new Toplevel ();
+        top.Add (lv);
+        app.Begin (top);
+
+        DriverAssert.AssertDriverContentsWithFrameAre (
+                                                       @"
+ Item 0
+ Item 1
+ Item 2
+ Item 3
+ Item 4",
+                                                       _output, app.Driver);
+
+        lv.LeftItem = 1;
+        lv.TopItem = 1;
+        app.LayoutAndDraw ();
+
+        DriverAssert.AssertDriverContentsWithFrameAre (
+                                                       @"
+ tem 1
+ tem 2
+ tem 3
+ tem 4",
+                                                       _output, app.Driver);
+        top.Dispose ();
+        app.Shutdown ();
+    }
+
+    [Fact]
+    public void RowRender_Event ()
+    {
+        IApplication? app = Application.Create ();
+        app.Init ("fake");
+
+        var rendered = false;
+        ObservableCollection<string> source = ["one", "two", "three"];
+        var lv = new ListView { Width = Dim.Fill (), Height = Dim.Fill () };
+        lv.RowRender += (s, _) => rendered = true;
+        var top = new Toplevel ();
+        top.Add (lv);
+        app.Begin (top);
+        Assert.False (rendered);
+
+        lv.SetSource (source);
+        lv.Draw ();
+        Assert.True (rendered);
+        top.Dispose ();
+        app.Shutdown ();
+    }
+
+    [Fact]
+    public void Vertical_ScrollBar_Hides_And_Shows_As_Needed ()
+    {
+        IApplication? app = Application.Create ();
+        app.Init ("fake");
+
+        var lv = new ListView
+        {
+            Width = 10,
+            Height = 3
+        };
+        lv.VerticalScrollBar.AutoShow = true;
+        lv.SetSource (["One", "Two", "Three", "Four", "Five"]);
+        var top = new Toplevel ();
+        top.Add (lv);
+        app.Begin (top);
+
+        Assert.True (lv.VerticalScrollBar.Visible);
+        DriverAssert.AssertDriverContentsWithFrameAre (
+                                                       @"
+One      ▲
+Two      █
+Three    ▼",
+                                                       _output, app?.Driver);
+
+        lv.Height = 5;
+        app?.LayoutAndDraw ();
+
+        Assert.False (lv.VerticalScrollBar.Visible);
+        DriverAssert.AssertDriverContentsWithFrameAre (
+                                                       @"
+One  
+Two  
+Three
+Four 
+Five ",
+                                                       _output, app?.Driver);
+        top.Dispose ();
+        app?.Shutdown ();
+    }
+
+    [Fact]
+    public void Mouse_Wheel_Scrolls ()
+    {
+        IApplication? app = Application.Create ();
+        app.Init ("fake");
+
+        var lv = new ListView
+        {
+            Width = 10,
+            Height = 3,
+        };
+        lv.SetSource (["One", "Two", "Three", "Four", "Five"]);
+        var top = new Toplevel ();
+        top.Add (lv);
+        app.Begin (top);
+
+        // Initially, we are at the top.
+        Assert.Equal (0, lv.TopItem);
+        DriverAssert.AssertDriverContentsWithFrameAre (
+                                                       @"
+One  
+Two  
+Three",
+                                                       _output, app?.Driver);
+
+        // Scroll down
+        app.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.WheeledDown });
+        app.LayoutAndDraw ();
+        Assert.Equal (1, lv.TopItem);
+        DriverAssert.AssertDriverContentsWithFrameAre (
+                                                       @"
+Two  
+Three
+Four ",
+                                                       _output, app?.Driver);
+
+        // Scroll up
+        app.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.WheeledUp });
+        app.LayoutAndDraw ();
+        Assert.Equal (0, lv.TopItem);
+        DriverAssert.AssertDriverContentsWithFrameAre (
+                                                       @"
+One  
+Two  
+Three",
+                                                       _output, app?.Driver);
+
+        top.Dispose ();
+        app.Shutdown ();
+    }
+
+    [Fact]
+    public void SelectedItem_With_Source_Null_Does_Nothing ()
+    {
+        var lv = new ListView ();
+        Assert.Null (lv.Source);
+
+        // should not throw
+        lv.SelectedItem = 0;
+
+        Assert.Null (lv.SelectedItem);
+    }
+
+    [Fact]
+    public void Horizontal_Scroll ()
+    {
+        IApplication? app = Application.Create ();
+        app.Init ("fake");
+
+        var lv = new ListView
+        {
+            Width = 10,
+            Height = 3,
+        };
+        lv.SetSource (["One", "Two", "Three - long", "Four", "Five"]);
+        var top = new Toplevel ();
+        top.Add (lv);
+        app.Begin (top);
+
+        Assert.Equal (0, lv.LeftItem);
+        DriverAssert.AssertDriverContentsWithFrameAre (
+                                                       @"
+One       
+Two       
+Three - lo",
+                                                       _output, app?.Driver);
+
+        lv.ScrollHorizontal (1);
+        app.LayoutAndDraw ();
+        Assert.Equal (1, lv.LeftItem);
+        DriverAssert.AssertDriverContentsWithFrameAre (
+                                                       @"
+ne        
+wo        
+hree - lon",
+                                                       _output, app?.Driver);
+
+        // Scroll right with mouse
+        app.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.WheeledRight });
+        app.LayoutAndDraw ();
+        Assert.Equal (2, lv.LeftItem);
+        DriverAssert.AssertDriverContentsWithFrameAre (
+                                                       @"
+e         
+o         
+ree - long",
+                                                       _output, app?.Driver);
+
+        // Scroll left with mouse
+        app.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.WheeledLeft });
+        app.LayoutAndDraw ();
+        Assert.Equal (1, lv.LeftItem);
+        DriverAssert.AssertDriverContentsWithFrameAre (
+                                                       @"
+ne        
+wo        
+hree - lon",
+                                                       _output, app?.Driver);
+
+        top.Dispose ();
+        app.Shutdown ();
+    }
+
+    [Fact]
+    public async Task SetSourceAsync_SetsSource ()
+    {
+        var lv = new ListView ();
+        var source = new ObservableCollection<string> { "One", "Two", "Three" };
+
+        await lv.SetSourceAsync (source);
+
+        Assert.NotNull (lv.Source);
+        Assert.Equal (3, lv.Source.Count);
+    }
+
+    [Fact]
+    public void AllowsMultipleSelection_Set_To_False_Unmarks_All_But_Selected ()
+    {
+        var lv = new ListView { AllowsMarking = true, AllowsMultipleSelection = true };
+        var source = new ListWrapper<string> (["One", "Two", "Three"]);
+        lv.Source = source;
+
+        lv.SelectedItem = 0;
+        source.SetMark (0, true);
+        source.SetMark (1, true);
+        source.SetMark (2, true);
+
+        Assert.True (source.IsMarked (0));
+        Assert.True (source.IsMarked (1));
+        Assert.True (source.IsMarked (2));
+
+        lv.AllowsMultipleSelection = false;
+
+        Assert.True (source.IsMarked (0));
+        Assert.False (source.IsMarked (1));
+        Assert.False (source.IsMarked (2));
+    }
+
+    [Fact]
+    public void Source_CollectionChanged_Remove ()
+    {
+        var source = new ObservableCollection<string> { "One", "Two", "Three" };
+        var lv = new ListView { Source = new ListWrapper<string> (source) };
+
+        lv.SelectedItem = 2;
+        Assert.Equal (2, lv.SelectedItem);
+        Assert.Equal (3, lv.Source.Count);
+
+        source.RemoveAt (0);
+
+        Assert.Equal (2, lv.Source.Count);
+        Assert.Equal (1, lv.SelectedItem);
+
+        source.RemoveAt (1);
+        Assert.Equal (1, lv.Source.Count);
+        Assert.Equal (0, lv.SelectedItem);
     }
 }

+ 5 - 5
Tests/UnitTestsParallelizable/Views/NumericUpDownTests.cs

@@ -112,7 +112,7 @@ public class NumericUpDownTests
     public void WhenCreated_ShouldHaveDefaultWidthAndHeight_int ()
     {
         NumericUpDown<int> numericUpDown = new ();
-        numericUpDown.SetRelativeLayout (Application.Screen.Size);
+        numericUpDown.SetRelativeLayout (new (100, 100));
 
         Assert.Equal (3, numericUpDown.Frame.Width);
         Assert.Equal (1, numericUpDown.Frame.Height);
@@ -122,7 +122,7 @@ public class NumericUpDownTests
     public void WhenCreated_ShouldHaveDefaultWidthAndHeight_float ()
     {
         NumericUpDown<float> numericUpDown = new ();
-        numericUpDown.SetRelativeLayout (Application.Screen.Size);
+        numericUpDown.SetRelativeLayout (new (100, 100));
 
         Assert.Equal (3, numericUpDown.Frame.Width);
         Assert.Equal (1, numericUpDown.Frame.Height);
@@ -132,7 +132,7 @@ public class NumericUpDownTests
     public void WhenCreated_ShouldHaveDefaultWidthAndHeight_double ()
     {
         NumericUpDown<double> numericUpDown = new ();
-        numericUpDown.SetRelativeLayout (Application.Screen.Size);
+        numericUpDown.SetRelativeLayout (new (100, 100));
 
         Assert.Equal (3, numericUpDown.Frame.Width);
         Assert.Equal (1, numericUpDown.Frame.Height);
@@ -142,7 +142,7 @@ public class NumericUpDownTests
     public void WhenCreated_ShouldHaveDefaultWidthAndHeight_long ()
     {
         NumericUpDown<long> numericUpDown = new ();
-        numericUpDown.SetRelativeLayout (Application.Screen.Size);
+        numericUpDown.SetRelativeLayout (new (100, 100));
 
         Assert.Equal (3, numericUpDown.Frame.Width);
         Assert.Equal (1, numericUpDown.Frame.Height);
@@ -152,7 +152,7 @@ public class NumericUpDownTests
     public void WhenCreated_ShouldHaveDefaultWidthAndHeight_decimal ()
     {
         NumericUpDown<decimal> numericUpDown = new ();
-        numericUpDown.SetRelativeLayout (Application.Screen.Size);
+        numericUpDown.SetRelativeLayout (new (100, 100));
 
         Assert.Equal (3, numericUpDown.Frame.Width);
         Assert.Equal (1, numericUpDown.Frame.Height);

+ 34 - 22
docfx/docs/application.md

@@ -8,7 +8,7 @@ Terminal.Gui v2 uses an instance-based application architecture that decouples v
 graph TB
     subgraph ViewTree["View Hierarchy (SuperView/SubView)"]
         direction TB
-        Top[Application.Current<br/>Window]
+        Top[app.Current<br/>Window]
         Menu[MenuBar]
         Status[StatusBar]
         Content[Content View]
@@ -22,7 +22,7 @@ graph TB
         Content --> Button2
     end
     
-    subgraph Stack["Application.SessionStack"]
+    subgraph Stack["app.SessionStack"]
         direction TB
         S1[Window<br/>Currently Active]
         S2[Previous Toplevel<br/>Waiting]
@@ -41,7 +41,7 @@ graph TB
 
 ```mermaid
 sequenceDiagram
-    participant App as Application
+    participant App as IApplication
     participant Main as Main Window
     participant Dialog as Dialog
     
@@ -68,24 +68,29 @@ sequenceDiagram
 
 ### Instance-Based vs Static
 
-**Terminal.Gui v2** has transitioned from a static singleton pattern to an instance-based architecture:
+**Terminal.Gui v2** supports both static and instance-based patterns. The static `Application` class is marked obsolete but still functional for backward compatibility. The recommended pattern is to use `Application.Create()` to get an `IApplication` instance:
 
 ```csharp
-// OLD (v1 / early v2 - now obsolete):
+// OLD (v1 / early v2 - still works but obsolete):
 Application.Init();
-Application.Top.Add(myView);
-Application.Run();
+var top = new Toplevel();
+top.Add(myView);
+Application.Run(top);
+top.Dispose();
 Application.Shutdown();
 
-// NEW (v2 instance-based):
-var app = Application.Create ();
+// NEW (v2 recommended - instance-based):
+var app = Application.Create();
 app.Init();
 var top = new Toplevel();
 top.Add(myView);
 app.Run(top);
+top.Dispose();
 app.Shutdown();
 ```
 
+**Note:** The static `Application` class delegates to `ApplicationImpl.Instance` (a singleton). `Application.Create()` creates a **new** `ApplicationImpl` instance, enabling multiple application contexts and better testability.
+
 ### View.App Property
 
 Every view now has an `App` property that references its application context:
@@ -226,19 +231,23 @@ int sessionCount = App?.SessionStack.Count ?? 0;
 
 ## Migration from Static Application
 
-The static `Application` class now delegates to `ApplicationImpl.Instance` and is marked obsolete:
+The static `Application` class delegates to `ApplicationImpl.Instance` (a singleton) and is marked obsolete. All static methods and properties are marked with `[Obsolete]` but remain functional for backward compatibility:
 
 ```csharp
-public static class Application
+public static partial class Application
 {
-    [Obsolete("Use ApplicationImpl.Instance.Current or view.App?.Current")]
-    public static Toplevel? Current => Instance?.Current;
+    [Obsolete("The legacy static Application object is going away.")]
+    public static Toplevel? Current => ApplicationImpl.Instance.Current;
     
-    [Obsolete("Use ApplicationImpl.Instance.SessionStack or view.App?.SessionStack")]
-    public static ConcurrentStack<Toplevel> SessionStack => Instance?.SessionStack ?? new();
+    [Obsolete("The legacy static Application object is going away.")]
+    public static ConcurrentStack<Toplevel> SessionStack => ApplicationImpl.Instance.SessionStack;
+    
+    // ... other obsolete static members
 }
 ```
 
+**Important:** The static `Application` class uses a singleton (`ApplicationImpl.Instance`), while `Application.Create()` creates new instances. For new code, prefer the instance-based pattern using `Application.Create()`.
+
 ### Migration Strategies
 
 **Strategy 1: Use View.App**
@@ -472,16 +481,19 @@ public class Service
 }
 ```
 
-### DON'T: Assume Application.Instance Exists
+### DON'T: Use Static Application in New Code
 
 ```csharp
-❌ AVOID:
-public class Service
+❌ AVOID (obsolete pattern):
+public void Refresh()
 {
-    public void DoWork()
-    {
-        var app = Application.Instance; // Might be null!
-    }
+    Application.Current?.SetNeedsDraw(); // Obsolete static access
+}
+
+✅ PREFERRED:
+public void Refresh()
+{
+    App?.Current?.SetNeedsDraw(); // Use View.App property
 }
 ```
 

+ 2 - 1
docfx/docs/config.md

@@ -459,7 +459,8 @@ ThemeManager.ThemeChanged += (sender, e) =>
 {
     // Theme has changed
     // Refresh all views to use new theme
-    Application.Current?.SetNeedsDraw();
+    // From within a View, use: App?.Current?.SetNeedsDraw();
+    // Or access via IApplication instance: app.Current?.SetNeedsDraw();
 };
 ```
 

+ 8 - 0
docfx/docs/index.md

@@ -13,10 +13,13 @@ Welcome to the Terminal.Gui documentation! This comprehensive guide covers every
 - [Getting Started](~/docs/getting-started.md) - Quick start guide to create your first Terminal.Gui application
 - [Migrating from v1 to v2](~/docs/migratingfromv1.md) - Complete guide for upgrading existing applications
 - [What's New in v2](~/docs/newinv2.md) - Overview of new features and improvements
+- [Showcase](~/docs/showcase.md) - Showcase of TUI apps built with Terminal.Gui
 
 ## Deep Dives
 
 - [ANSI Response Parser](~/docs/ansiparser.md) - Terminal sequence parsing and state management
+- [Application](~/docs/application.md) - Application lifecycle, initialization, and main loop
+- [Arrangement](~/docs/arrangement.md) - View arrangement and positioning strategies
 - [Cancellable Work Pattern](~/docs/cancellable-work-pattern.md) - Core design pattern for extensible workflows
 - [Character Map Scenario](~/docs/CharacterMap.md) - Complex drawing, scrolling, and Unicode rendering example
 - [Command System](~/docs/command.md) - Command execution, key bindings, and the Selecting/Accepting concepts
@@ -24,6 +27,7 @@ Welcome to the Terminal.Gui documentation! This comprehensive guide covers every
 - [Cross-Platform Driver Model](~/docs/drivers.md) - Platform abstraction and console driver architecture
 - [Cursor System](~/docs/cursor.md) - Modern cursor management and positioning (proposed design)
 - [Dim.Auto](~/docs/dimauto.md) - Automatic view sizing based on content
+- [Drawing](~/docs/drawing.md) - Drawing primitives, rendering, and graphics operations
 - [Events](~/docs/events.md) - Event patterns and handling throughout the framework
 - [Keyboard Input](~/docs/keyboard.md) - Key handling, bindings, commands, and shortcuts
 - [Layout System](~/docs/layout.md) - View positioning, sizing, and arrangement
@@ -33,7 +37,11 @@ Welcome to the Terminal.Gui documentation! This comprehensive guide covers every
 - [Mouse Input](~/docs/mouse.md) - Mouse event handling and interaction patterns
 - [Navigation](~/docs/navigation.md) - Focus management, keyboard navigation, and accessibility
 - [Popovers](~/docs/Popovers.md) - Drawing outside viewport boundaries for menus and popups
+- [Scheme](~/docs/scheme.md) - Color schemes, styling, and visual theming
 - [Scrolling](~/docs/scrolling.md) - Built-in scrolling, virtual content areas, and scroll bars
+- [TableView](~/docs/tableview.md) - Table view component, data binding, and column management
+- [TreeView](~/docs/treeview.md) - Tree view component, hierarchical data, and node management
+- [View](~/docs/View.md) - Base view class, view hierarchy, and core view functionality
 
 ## API Reference
 

+ 68 - 0
docfx/docs/migratingfromv1.md

@@ -93,6 +93,74 @@ In v1, @Terminal.Gui./Terminal.Gui.Application.Init) automatically created a top
 * Update any code that assumes `Application.Init` automatically created a toplevel view and set `Application.Current`.
 * Update any code that assumes `Application.Init` automatically disposed of the toplevel view when the application exited.
 
+## Instance-Based Application Architecture
+
+See the [Application Deep Dive](application.md) for complete details on the new application architecture.
+
+Terminal.Gui v2 introduces an instance-based application architecture. While the static `Application` class still works (marked obsolete), the recommended pattern is to use `Application.Create()` to get an `IApplication` instance.
+
+### Key Changes
+
+- **Static Application is Obsolete**: The static `Application` class delegates to `ApplicationImpl.Instance` (a singleton) and is marked `[Obsolete]` but remains functional for backward compatibility.
+- **Recommended Pattern**: Use `Application.Create()` to get a new `IApplication` instance for better testability and multiple application contexts.
+- **View.App Property**: Every view has an `App` property that references its `IApplication` context, enabling views to access application services without static dependencies.
+
+### Migration Strategies
+
+**Option 1: Continue Using Static Application (Backward Compatible)**
+
+The static `Application` class still works, so existing v1 code can continue to work with minimal changes:
+
+```csharp
+// v1 code (still works in v2, but obsolete)
+Application.Init();
+var top = new Toplevel();
+top.Add(myView);
+Application.Run(top);
+top.Dispose();
+Application.Shutdown();
+```
+
+**Option 2: Migrate to Instance-Based Pattern (Recommended)**
+
+For new code or when refactoring, use the instance-based pattern:
+
+```csharp
+// v2 recommended pattern
+var app = Application.Create();
+app.Init();
+var top = new Toplevel();
+top.Add(myView);
+app.Run(top);
+top.Dispose();
+app.Shutdown();
+```
+
+**Option 3: Use View.App Property**
+
+When accessing application services from within views, use the `App` property instead of static `Application`:
+
+```csharp
+// OLD (v1 / obsolete static):
+public void Refresh()
+{
+    Application.Current?.SetNeedsDraw();
+}
+
+// NEW (v2 - use View.App):
+public void Refresh()
+{
+    App?.Current?.SetNeedsDraw();
+}
+```
+
+### Benefits of Instance-Based Architecture
+
+- **Testability**: Views can be tested without `Application.Init()` by setting `view.App = mockApp`
+- **Multiple Contexts**: Multiple `IApplication` instances can coexist
+- **Clear Ownership**: Views explicitly know their application context
+- **Reduced Global State**: Less reliance on static singletons
+
 ## @Terminal.Gui.Pos and @Terminal.Gui.Dim types now adhere to standard C# idioms
 
 * In v1, the @Terminal.Gui.Pos and @Terminal.Gui.Dim types (e.g. @Terminal.Gui.Pos.PosView) were nested classes and marked @Terminal.Gui.internal. In v2, they are no longer nested, and have appropriate public APIs. 

+ 50 - 6
docfx/docs/multitasking.md

@@ -9,7 +9,7 @@ Terminal.Gui applications run on a single main thread with an event loop that pr
 Terminal.Gui follows the standard UI toolkit pattern where **all UI operations must happen on the main thread**. Attempting to modify views or their properties from background threads will result in undefined behavior and potential crashes.
 
 ### The Golden Rule
-> Always use `Application.Invoke()` to update the UI from background threads.
+> Always use `Application.Invoke()` (static, obsolete) or `app.Invoke()` (instance-based, recommended) to update the UI from background threads. From within a View, use `App?.Invoke()`.
 
 ## Background Operations
 
@@ -47,6 +47,7 @@ private async void LoadDataButton_Clicked()
 
 When working with traditional threading APIs or when async/await isn't suitable:
 
+**From within a View (recommended):**
 ```csharp
 private void StartBackgroundWork()
 {
@@ -58,14 +59,14 @@ private void StartBackgroundWork()
             Thread.Sleep(50); // Simulate work
             
             // Marshal back to main thread for UI updates
-            Application.Invoke(() =>
+            App?.Invoke(() =>
             {
                 progressBar.Fraction = i / 100f;
                 statusLabel.Text = $"Progress: {i}%";
             });
         }
         
-        Application.Invoke(() =>
+        App?.Invoke(() =>
         {
             statusLabel.Text = "Complete!";
         });
@@ -73,6 +74,41 @@ private void StartBackgroundWork()
 }
 ```
 
+**Using IApplication instance (recommended):**
+```csharp
+var app = Application.Create();
+app.Init();
+
+private void StartBackgroundWork(IApplication app)
+{
+    Task.Run(() =>
+    {
+        // This code runs on a background thread
+        for (int i = 0; i <= 100; i++)
+        {
+            Thread.Sleep(50); // Simulate work
+            
+            // Marshal back to main thread for UI updates
+            app.Invoke(() =>
+            {
+                progressBar.Fraction = i / 100f;
+                statusLabel.Text = $"Progress: {i}%";
+            });
+        }
+        
+        app.Invoke(() =>
+        {
+            statusLabel.Text = "Complete!";
+        });
+    });
+}
+```
+
+**Using static Application (obsolete but still works):**
+```csharp
+Application.Invoke(() => { /* ... */ });
+```
+
 ## Timers
 
 Use timers for periodic updates like clocks, status refreshes, or animations:
@@ -89,10 +125,11 @@ public class ClockView : View
         Add(timeLabel);
         
         // Update every second
-        timerToken = Application.AddTimeout(
+        // Use App?.AddTimeout() when available, or Application.AddTimeout() (obsolete)
+        timerToken = App?.AddTimeout(
             TimeSpan.FromSeconds(1), 
             UpdateTime
-        );
+        ) ?? Application.AddTimeout(TimeSpan.FromSeconds(1), UpdateTime);
     }
     
     private bool UpdateTime()
@@ -105,7 +142,7 @@ public class ClockView : View
     {
         if (disposing && timerToken != null)
         {
-            Application.RemoveTimeout(timerToken);
+            App?.RemoveTimeout(timerToken) ?? Application.RemoveTimeout(timerToken);
         }
         base.Dispose(disposing);
     }
@@ -220,6 +257,13 @@ Task.Run(() =>
 ### ❌ Don't: Forget to clean up timers
 ```csharp
 // Memory leak - timer keeps running after view is disposed
+// From within a View:
+App?.AddTimeout(TimeSpan.FromSeconds(1), UpdateStatus);
+
+// Or with IApplication instance:
+app.AddTimeout(TimeSpan.FromSeconds(1), UpdateStatus);
+
+// Or static (obsolete but works):
 Application.AddTimeout(TimeSpan.FromSeconds(1), UpdateStatus);
 ```
 

+ 10 - 5
docfx/docs/navigation.md

@@ -176,25 +176,30 @@ The @Terminal.Gui.App.ApplicationNavigation.AdvanceFocus method causes the focus
 The implementation is simple:
 
 ```cs
-return Application.Current?.AdvanceFocus (direction, behavior);
+return app.Current?.AdvanceFocus (direction, behavior);
 ```
 
-This method is called from the `Command` handlers bound to the application-scoped keybindings created during `Application.Init`. It is `public` as a convenience.
+This method is called from the `Command` handlers bound to the application-scoped keybindings created during `app.Init()`. It is `public` as a convenience.
+
+**Note:** When accessing from within a View, use `App?.Current` instead of `Application.Current` (which is obsolete).
 
 This method replaces about a dozen functions in v1 (scattered across `Application` and `Toplevel`).
 
 ### Application Navigation Examples
 
 ```csharp
+var app = Application.Create();
+app.Init();
+
 // Listen for global focus changes
-Application.Navigation.FocusedChanged += (sender, e) => 
+app.Navigation.FocusedChanged += (sender, e) => 
 {
-    var focused = Application.Navigation.GetFocused();
+    var focused = app.Navigation.GetFocused();
     StatusBar.Text = $"Focused: {focused?.GetType().Name ?? "None"}";
 };
 
 // Prevent certain views from getting focus
-Application.Navigation.FocusedChanging += (sender, e) => 
+app.Navigation.FocusedChanging += (sender, e) => 
 {
     if (e.NewView is SomeRestrictedView)
     {

+ 41 - 0
docfx/docs/newinv2.md

@@ -15,6 +15,47 @@ Terminal.Gui v2 represents a fundamental rethinking of the library's architectur
 
 This architectural shift has resulted in the removal of thousands of lines of redundant or overly complex code from v1, replaced with cleaner, more focused implementations.
 
+## Instance-Based Application Architecture
+
+See the [Application Deep Dive](application.md) for complete details on the new application architecture.
+
+Terminal.Gui v2 introduces an instance-based application architecture that decouples views from global application state, dramatically improving testability and enabling multiple application contexts.
+
+### Key Changes
+
+- **Instance-Based Pattern**: The recommended pattern is to use `Application.Create()` to get an `IApplication` instance, rather than using the static `Application` class (which is marked obsolete but still functional for backward compatibility).
+- **View.App Property**: Every view now has an `App` property that references its `IApplication` context, enabling views to access application services without static dependencies.
+- **Session Management**: Applications manage sessions through `Begin()` and `End()` methods, with a `SessionStack` tracking nested sessions and `Current` representing the active session.
+- **Improved Testability**: Views can be tested in isolation by setting their `App` property to a mock `IApplication`, eliminating the need for `Application.Init()` in unit tests.
+
+### Example Usage
+
+```csharp
+// Recommended v2 pattern (instance-based)
+var app = Application.Create();
+app.Init();
+var top = new Toplevel { Title = "My App" };
+top.Add(myView);
+app.Run(top);
+top.Dispose();
+app.Shutdown();
+
+// Static pattern (obsolete but still works)
+Application.Init();
+var top = new Toplevel { Title = "My App" };
+top.Add(myView);
+Application.Run(top);
+top.Dispose();
+Application.Shutdown();
+```
+
+### Benefits
+
+- **Testability**: Views can be tested without initializing the entire application
+- **Multiple Contexts**: Multiple `IApplication` instances can coexist (useful for testing or complex scenarios)
+- **Clear Ownership**: Views explicitly know their application context via the `App` property
+- **Reduced Global State**: Less reliance on static singletons improves code maintainability
+
 ## Modern Look & Feel - Technical Details
 
 ### TrueColor Support

+ 12 - 8
docfx/docs/toc.yml

@@ -2,10 +2,16 @@
   href: index.md
 - name: Getting Started
   href: getting-started.md
+- name: Showcase
+  href: showcase.md
 - name: What's new in v2
   href: newinv2.md
 - name: v1 To v2 Migration
   href: migratingfromv1.md  
+- name: Lexicon & Taxonomy
+  href: lexicon.md
+- name: Application Deep Dive
+  href: application.md
 - name: Arrangement
   href: arrangement.md
 - name: Cancellable Work Pattern
@@ -24,10 +30,6 @@
   href: drivers.md
 - name: Events Deep Dive
   href: events.md
-- name: Lexicon & Taxonomy
-  href: lexicon.md
-- name: Terminology Proposal
-  href: terminology-index.md
 - name: Keyboard
   href: keyboard.md
 - name: Layout Engine
@@ -40,14 +42,16 @@
   href: navigation.md
 - name: Popovers
   href: Popovers.md
-- name: View Deep Dive
-  href: View.md  
-- name: View List
-  href: views.md
+- name: Scheme Deep Dive
+  href: scheme.md
 - name: Scrolling
   href: scrolling.md
 - name: TableView Deep Dive
   href: tableview.md
 - name: TreeView Deep Dive
   href: treeview.md
+- name: View Deep Dive
+  href: View.md  
+- name: View List
+  href: views.md
 

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff